From f38f8c7f0412312c9a891ea70b5a024ed7cabce0 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 12:55:33 -0400 Subject: [PATCH 01/94] WIP: verify-contract generator from a SQL schema and demonstrations --- .../openadapt_flow/generate/demo_analysis.py | 625 ++++++ .../flow/openadapt_flow/generate/sql_store.py | 518 +++++ .../generate/verify_contract.py | 1912 +++++++++++++++++ .../tests/test_generate_verify_contract.py | 335 +++ 4 files changed, 3390 insertions(+) create mode 100644 packages/flow/openadapt_flow/generate/demo_analysis.py create mode 100644 packages/flow/openadapt_flow/generate/sql_store.py create mode 100644 packages/flow/openadapt_flow/generate/verify_contract.py create mode 100644 packages/flow/tests/test_generate_verify_contract.py diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py new file mode 100644 index 0000000000..404033242f --- /dev/null +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -0,0 +1,625 @@ +"""Find where each parameter of a kind of write lands, from demonstration diffs. + +A demonstration is one correct write of a kind, made by the person or the +setup agent, with the store snapshotted before and after +(:mod:`openadapt_flow.generate.sql_store`). This module reads the diffs and +says, for each parameter, which table and column holds its value, and by +which exact rule: + +- ``exact``: the stored text equals the value's text; +- ``email``: equal after case folding (``ValueExpr`` kind ``email``); +- ``phone``: equal digits (``ValueExpr`` kind ``phone``); +- ``number``, ``date``, ``datetime``: equal by value, compared in SQL after a + cast of the parameter to the column's type; +- ``minutes``: the number of minutes between two date-time columns of the + row; +- a display column of a row another column points to (a declared foreign + key, or a column whose value is that row's primary key), or two text + columns joined by one space (a person's first and last name); +- a row the write added beside the main record, linked to it by a column + that holds the main record's key. + +Every rule is exact: no fuzzy matching, no model. A parameter found in more +than one place in every demonstration, or in none, is reported, and the +generator turns it into a question for a person. +""" + +from __future__ import annotations + +import datetime as _dt +import decimal +import re +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from typing import Any, Optional + +from openadapt_flow.generate.sql_store import ( + Diff, + RowChange, + Schema, + Table, + TableRows, +) +from openadapt_flow.runtime.effects.effect import kind_form + +#: Rules a value can be found by, most exact first. +RULES = ("exact", "email", "phone", "number", "date", "datetime", "minutes") + +_IDLIKE = re.compile(r"(^id$|_id$|^id_|^fk_|_fk$|^parent$|^parent_id$)", re.I) +_IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +#: Table names an app's own logs usually carry. Only a proposal: a person +#: confirms which tables are logs (the bookkeeping question). +_LOG_NAME = re.compile( + r"(log|history|audit|session|token|notification|queue|job|stat|cache|lock|event)", + re.I, +) + + +def value_text(value: Any) -> str: + """A parameter value as text, the way the verifier renders it.""" + + from openadapt_flow.runtime.authorization import runtime_param_text + + if isinstance(value, str): + return value + return runtime_param_text(value) + + +def _decimal(text: Any) -> Optional[decimal.Decimal]: + if text is None or isinstance(text, bool): + return None + try: + d = decimal.Decimal(str(text).strip()) + except (decimal.InvalidOperation, ValueError): + return None + return d if d.is_finite() else None + + +def _datetime(text: str) -> Optional[_dt.datetime]: + for fmt in ( + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%dT%H:%M", + "%Y-%m-%dT%H:%M:%SZ", + "%Y-%m-%dT%H:%MZ", + ): + try: + return _dt.datetime.strptime(text.strip(), fmt) + except ValueError: + continue + return None + + +def rule_for(stored: Optional[str], column_kind: str, value: Any) -> Optional[str]: + """The most exact rule by which ``stored`` holds ``value``, or None.""" + + if stored is None or value is None or isinstance(value, (list, dict)): + return None + text = value_text(value) + if text == "": + return None + if stored == text: + return "exact" + if column_kind == "text": + if "@" in text and stored.casefold() == text.casefold(): + return "email" + digits = sum(ch.isdigit() for ch in text) + if digits >= 6: + a, b = kind_form("phone", stored), kind_form("phone", text) + if a is not None and a == b: + return "phone" + return None + if column_kind in ("number", "bool"): + a, b = _decimal(stored), _decimal(text) + if a is not None and b is not None and a == b: + return "number" + return None + if column_kind == "date": + return "date" if stored[:10] == text and len(text) == 10 else None + if column_kind == "datetime": + a, b = _datetime(stored.split(".")[0]), _datetime(text) + if a is not None and b is not None and a == b: + return "datetime" + return None + return None + + +@dataclass(frozen=True) +class Hop: + """One step from a row to another row. + + ``fk``: this row's ``column`` holds the key ``ref_column`` of a row of + ``table``. ``child``: a row of ``table`` whose ``column`` holds this row's + primary key ``ref_column``. + """ + + kind: str # fk | child + table: str + column: str + ref_column: str + declared: bool = False + + def to_json(self) -> dict[str, Any]: + return { + "kind": self.kind, + "table": self.table, + "column": self.column, + "ref_column": self.ref_column, + "declared": self.declared, + } + + @classmethod + def from_json(cls, d: Mapping[str, Any]) -> "Hop": + return cls( + str(d["kind"]), + str(d["table"]), + str(d["column"]), + str(d["ref_column"]), + bool(d.get("declared", False)), + ) + + +@dataclass(frozen=True) +class Location: + """Where a value is: a path of hops from the main row, then the column + (or two columns joined by a space) that holds it, and the rule.""" + + hops: tuple[Hop, ...] + columns: tuple[str, ...] + rule: str + + @property + def table_hint(self) -> str: + return self.hops[-1].table if self.hops else "" + + def signature(self) -> str: + path = "/".join( + f"{h.kind}:{h.table}.{h.column}={h.ref_column}" for h in self.hops + ) + return f"{path}#{'+'.join(self.columns)}@{self.rule}" + + def describe(self, anchor: str) -> str: + where = anchor + for h in self.hops: + if h.kind == "fk": + where = f"{where}.{h.column} -> {h.table}" + else: + where = f"{h.table} (whose {h.column} is the {where} key)" + cols = " || ' ' || ".join(self.columns) + return f"{where}.{cols} ({self.rule})" + + def to_json(self) -> dict[str, Any]: + return { + "hops": [h.to_json() for h in self.hops], + "columns": list(self.columns), + "rule": self.rule, + } + + @classmethod + def from_json(cls, d: Mapping[str, Any]) -> "Location": + return cls( + tuple(Hop.from_json(h) for h in d.get("hops", [])), + tuple(d["columns"]), + str(d["rule"]), + ) + + +def _hop_cost(h: Hop) -> int: + """0 for a declared foreign key, 1 for a column named after the table it + points to (``status_id`` -> ``issue_statuses``), 2 for a value match only.""" + + if h.declared: + return 0 + stem = re.sub(r"(^fk_|_id$|_fk$|^id_)", "", h.column.lower()).rstrip("s") + table = h.table.lower() + return 1 if stem and len(stem) >= 3 and stem in table else 2 + + +def _rank(loc: Location) -> tuple[int, int, int, int]: + return ( + len(loc.hops), + sum(_hop_cost(h) for h in loc.hops), + RULES.index(loc.rule) if loc.rule in RULES else 99, + len(loc.columns), + ) + + +@dataclass +class Demo: + """One demonstration: the kind, the values given, and the diff.""" + + kind: str + values: dict[str, Any] + diff: Diff + after: Mapping[str, TableRows] + started_at: str = "" + finished_at: str = "" + name: str = "" + + +class _Index: + """Rows by table and single-column key, from one snapshot.""" + + def __init__(self, schema: Schema, rows: Mapping[str, TableRows]) -> None: + self.schema = schema + self.rows = rows + self._by_key: dict[str, dict[str, dict[str, Optional[str]]]] = {} + for name, table in schema.tables.items(): + t = rows.get(name) + if t is None or t.count_only is not None or len(table.primary_key) != 1: + continue + pk = table.primary_key[0] + self._by_key[name] = { + str(r.get(pk)): r for r in t.rows.values() if r.get(pk) is not None + } + + def row(self, table: str, key: str) -> Optional[dict[str, Optional[str]]]: + return self._by_key.get(table, {}).get(key) + + def keyed_tables(self) -> list[str]: + return list(self._by_key) + + +def _fk_targets( + schema: Schema, idx: _Index, table: Table, column: str, value: str +) -> list[tuple[str, str, bool]]: + """Rows ``table.column = value`` may point to: (table, key column, + declared).""" + + out = [] + for fk in table.foreign_keys: + if fk.column == column and idx.row(fk.table, value) is not None: + target = schema.tables.get(fk.table) + if target is not None and target.primary_key == [fk.ref_column]: + out.append((fk.table, fk.ref_column, True)) + if out: + return out + if not _IDLIKE.search(column) or value in ("", "0"): + return [] + for name in idx.keyed_tables(): + if idx.row(name, value) is not None: + out.append((name, schema.tables[name].primary_key[0], False)) + return out + + +def _display_matches( + table: Table, row: Mapping[str, Optional[str]], value: Any +) -> list[tuple[tuple[str, ...], str]]: + """Columns of ``row`` that hold ``value``, with the rule.""" + + out: list[tuple[tuple[str, ...], str]] = [] + for col in table.columns: + rule = rule_for(row.get(col.name), col.kind, value) + if rule is not None: + out.append(((col.name,), rule)) + if isinstance(value, str) and " " in value.strip(): + texts = [c for c in table.columns if c.kind == "text"] + for i, a in enumerate(texts): + for b in texts[i + 1 : i + 3]: + va, vb = row.get(a.name), row.get(b.name) + if va and vb and f"{va} {vb}" == value: + out.append(((a.name, b.name), "exact")) + if isinstance(value, (int, float)) and not isinstance(value, bool): + stamps = [c for c in table.columns if c.kind == "datetime"] + for a in stamps: + for b in stamps: + if a is b: + continue + ta, tb = row.get(a.name), row.get(b.name) + da = _datetime((ta or "").split(".")[0]) if ta else None + db = _datetime((tb or "").split(".")[0]) if tb else None + if da and db and db > da: + minutes = decimal.Decimal((db - da).total_seconds()) / 60 + if minutes == _decimal(value_text(value)): + out.append(((a.name, b.name), "minutes")) + return out + + +def locate( + schema: Schema, + demo: Demo, + anchor: RowChange, + value: Any, + *, + before: bool = False, + changed_only: bool = False, + max_hops: int = 2, + links: Optional[Mapping[str, set[str]]] = None, +) -> list[Location]: + """Every location of ``value`` from the main row ``anchor``. + + ``before``: look in the row as it was before the write (an update's + identity). ``changed_only``: at depth 0, only columns the write changed + (an update's new values). + """ + + idx = _Index(schema, demo.after) + table = schema.tables[anchor.table] + row = anchor.before if before else anchor.after + if row is None: + return [] + found: list[Location] = [] + changed = set(anchor.changed_columns) + added = set() + for ch in demo.diff.changes: + t2 = schema.tables.get(ch.table) + if ch.kind == "added" and ch.after and t2 is not None and len(t2.primary_key) == 1: + added.add((ch.table, ch.after.get(t2.primary_key[0]))) + + def walk( + t: Table, r: Mapping[str, Optional[str]], hops: tuple[Hop, ...], depth: int + ) -> None: + for cols, rule in _display_matches(t, r, value): + if depth == 0 and changed_only and not (set(cols) & changed): + continue + found.append(Location(hops, cols, rule)) + if depth >= max_hops: + return + for col in t.columns: + v = r.get(col.name) + if v is None or (col.name in t.primary_key and hops): + continue + if depth == 0 and changed_only and col.name not in changed: + continue + for target, ref, declared in _fk_targets(schema, idx, t, col.name, v): + target_row = idx.row(target, v) + if target_row is None or (target == anchor.table and hops): + continue # back to the main record: not a location + if not declared and not hops and (target, v) in added: + # The main row's key columns point at rows that were + # there before; a row the write added is reached from a + # row that names the main record instead. + continue + walk( + schema.tables[target], + target_row, + hops + (Hop("fk", target, col.name, ref, declared),), + depth + 1, + ) + if depth == 0 and len(t.primary_key) == 1 and not before: + key = r.get(t.primary_key[0]) + if key is None: + return + for ch in demo.diff.changes: + if ch.kind != "added" or ch.table == t.name or ch.after is None: + continue + child = schema.tables.get(ch.table) + if child is None: + continue + for col in child.columns: + if ch.after.get(col.name) != key: + continue + if links is not None and col.name not in links.get(ch.table, {col.name}): + continue + declared = any( + fk.column == col.name and fk.table == t.name + for fk in child.foreign_keys + ) + if not declared and not _IDLIKE.search(col.name): + continue + walk( + child, + ch.after, + hops + + (Hop("child", ch.table, col.name, t.primary_key[0], declared),), + depth + 1, + ) + + walk(table, row, (), 0) + unique: dict[str, Location] = {} + for loc in found: + unique.setdefault(loc.signature(), loc) + return sorted(unique.values(), key=_rank) + + +def link_columns(schema: Schema, demos: Sequence[tuple["Demo", RowChange]]) -> dict[str, set[str]]: + """Per table, the columns that hold the main record's key in every row + that table gained, across all demonstrations (a journal's + ``journalized_id``, not its ``user_id`` that happens to be equal once).""" + + out: dict[str, set[str]] = {} + for demo, anchor in demos: + t = schema.tables.get(anchor.table) + row = anchor.after or {} + if t is None or len(t.primary_key) != 1 or row.get(t.primary_key[0]) is None: + continue + key = row[t.primary_key[0]] + for ch in demo.diff.changes: + if ch.kind != "added" or ch.table == anchor.table or ch.after is None: + continue + cols = {c for c, v in ch.after.items() if v == key} + out[ch.table] = out[ch.table] & cols if ch.table in out else cols + return out + + +def best(locations: Sequence[Location]) -> tuple[Optional[Location], list[Location]]: + """The single best location, or None and the tied ones.""" + + if not locations: + return None, [] + ranked = sorted(locations, key=_rank) + top = [loc for loc in ranked if _rank(loc) == _rank(ranked[0])] + if len(top) == 1: + return top[0], [] + return None, top + + +def pick_anchor( + schema: Schema, demo: Demo, params: Sequence[str] +) -> tuple[Optional[RowChange], str, int]: + """The main row of a demonstration: the added row (a create) or changed + row (an update) that holds the most parameter values. ``mode`` is + ``create`` or ``update``; ``score`` how many values it holds.""" + + best_row: Optional[RowChange] = None + best_mode, best_score, best_direct = "", 0, 0 + for ch in demo.diff.changes: + if ch.kind == "removed" or ch.table not in schema.tables: + continue + mode = "create" if ch.kind == "added" else "update" + score = direct = 0 + for p in params: + v = demo.values.get(p) + if v is None or isinstance(v, (list, dict)): + continue + if mode == "update": + hit = locate(schema, demo, ch, v, before=True, max_hops=1) or locate( + schema, demo, ch, v, changed_only=True, max_hops=1 + ) + else: + hit = locate(schema, demo, ch, v, max_hops=1) + score += 1 if hit else 0 + direct += 1 if any(not loc.hops for loc in hit) else 0 + if (score, direct) > (best_score, best_direct): + best_row, best_mode, best_score, best_direct = ch, mode, score, direct + return best_row, best_mode, best_score + + +def is_identifier(value: Any) -> bool: + return isinstance(value, str) and bool(_IDENT.match(value)) + + +def snake(name: str) -> str: + return re.sub(r"(? bool: + return bool(_LOG_NAME.search(table)) + + +@dataclass +class ColumnRole: + """What a column of a new row is, when no parameter explains it.""" + + role: str # key | timestamp | actor | link | copied | constant | varies | default + value: Optional[str] = None + detail: dict[str, Any] = field(default_factory=dict) + + +def _within(stamp: Optional[str], start: str, end: str) -> bool: + if not stamp or not start or not end: + return False + when = _datetime(stamp.split(".")[0].split("+")[0]) + if when is None: + try: + when = _dt.datetime.strptime(stamp[:10], "%Y-%m-%d") + except ValueError: + return False + lo = _dt.datetime.fromisoformat(start.replace("Z", "")) - _dt.timedelta(days=1) + hi = _dt.datetime.fromisoformat(end.replace("Z", "")) + _dt.timedelta(days=1) + return lo <= when <= hi + + +def column_roles( + schema: Schema, + table: str, + rows: Sequence[tuple[Demo, Mapping[str, Optional[str]]]], + explained: set[str], + actor_values: Mapping[str, str], + link_values: Sequence[set[str]] = (), +) -> dict[str, ColumnRole]: + """Roles of the columns of new rows of ``table`` that no parameter + explains, across every demonstration row given (one row per demo or + several). ``link_values``: per row, keys of rows the same write added + (a column holding one is a link).""" + + t = schema.tables[table] + out: dict[str, ColumnRole] = {} + for col in t.columns: + name = col.name + if name in explained: + continue + values = [r.get(name) for _, r in rows] + if col.auto or (name in t.primary_key and len(t.primary_key) == 1): + out[name] = ColumnRole("key") + continue + if col.kind in ("date", "datetime") and all( + v is None or _within(v, d.started_at, d.finished_at) + for (d, _), v in zip(rows, values) + ) and any(v is not None for v in values): + out[name] = ColumnRole("timestamp") + continue + actor = [ + p + for p, av in actor_values.items() + if values and all(v is not None and v == av for v in values) + ] + if actor: + out[name] = ColumnRole("actor", detail={"param": actor[0]}) + continue + links = link_values or [set() for _ in rows] + if values and all( + v is not None and (v in lv or (len(t.primary_key) == 1 and v == r.get(t.primary_key[0]))) + for v, lv, (_, r) in zip(values, links, rows) + ): + out[name] = ColumnRole("link") + continue + if len(t.primary_key) == 1 and values and all( + v is not None + and r.get(t.primary_key[0]) is not None + and str(r.get(t.primary_key[0])) in v + and v != r.get(t.primary_key[0]) + for v, (_, r) in zip(values, rows) + ): + out[name] = ColumnRole("varies", detail={"why": "holds the row's own key"}) + continue + copied = _copied(schema, t, name, rows) + if copied is not None: + out[name] = ColumnRole("copied", detail=copied) + continue + distinct = {v for v in values} + if len(distinct) == 1: + v = values[0] + default = col.default + if v is None: + out[name] = ColumnRole("constant", None) + else: + out[name] = ColumnRole( + "constant", v, detail={"is_default": default is not None and default.strip("'") == v} + ) + continue + out[name] = ColumnRole("varies") + return out + + +def _copied( + schema: Schema, + t: Table, + name: str, + rows: Sequence[tuple[Demo, Mapping[str, Optional[str]]]], +) -> Optional[dict[str, Any]]: + """Is the column a copy of one column of the row an FK points to, in + every row? (An invoice line's unit price is its product's price.)""" + + col = t.column(name) + if col is None or col.kind not in ("number", "text"): + return None + candidates: Optional[set[tuple[str, str, str, str]]] = None + for demo, r in rows: + v = r.get(name) + if v is None: + return None + idx = _Index(schema, demo.after) + here: set[tuple[str, str, str, str]] = set() + for other in t.columns: + if other.name == name or r.get(other.name) is None: + continue + for target, ref, _declared in _fk_targets( + schema, idx, t, other.name, str(r.get(other.name)) + ): + trow = idx.row(target, str(r.get(other.name))) + if trow is None: + continue + for tc in schema.tables[target].columns: + if tc.name == ref: + continue + if rule_for(trow.get(tc.name), tc.kind, v) in ("exact", "number"): + here.add((other.name, target, ref, tc.name)) + candidates = here if candidates is None else candidates & here + if not candidates: + return None + if candidates and len(candidates) == 1: + via, target, ref, source = next(iter(candidates)) + return {"via": via, "table": target, "ref_column": ref, "column": source} + return None diff --git a/packages/flow/openadapt_flow/generate/sql_store.py b/packages/flow/openadapt_flow/generate/sql_store.py new file mode 100644 index 0000000000..df0a7d666c --- /dev/null +++ b/packages/flow/openadapt_flow/generate/sql_store.py @@ -0,0 +1,518 @@ +"""Read a SQL database's schema and rows for the contract generator. + +The generator (:mod:`openadapt_flow.generate.verify_contract`) needs two +things from the store a write lands in: + +- the **schema**, from ``information_schema`` (MySQL, MariaDB, PostgreSQL): + every base table, its columns with their types, defaults and nullability, + the primary key, declared foreign keys, and the columns the database fills + on its own (auto-increment keys, ``ON UPDATE CURRENT_TIMESTAMP``); +- **row snapshots** before and after a demonstration write, so a diff shows + which tables, rows and columns one kind of write touches. + +Everything is read through the read-only connection the contract will use +(``sql_driver``, ``sql_connect_args``, ``sql_password_env``), with the +driver's read-only session switched on. Nothing here writes, and nothing +calls a model. + +A snapshot holds the rows' values. It is written only into the generator's +own working directory, which the person keeps private, like any database +export. +""" + +from __future__ import annotations + +import datetime as _dt +import decimal +import importlib +import json +import os +from collections.abc import Callable, Mapping, Sequence +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Any, Optional + +#: Dialects this module reads. +DIALECTS = ("mysql", "postgresql") + +#: Above this many rows a table is snapshotted by count and largest key only; +#: a demonstration diff can't say which of its rows changed. +DEFAULT_MAX_ROWS = 20000 + + +class StoreError(RuntimeError): + """The store can't be read as the generator needs.""" + + +@dataclass +class Column: + name: str + data_type: str + nullable: bool + default: Optional[str] + #: The database fills it on insert (auto-increment, a sequence, identity). + auto: bool = False + #: The database rewrites it on every update (``ON UPDATE CURRENT_TIMESTAMP``). + on_update: bool = False + + @property + def kind(self) -> str: + """``text``, ``number``, ``date``, ``datetime``, ``time``, ``bool``, + ``binary`` or ``other``.""" + + t = self.data_type.lower() + if t in ("tinyint(1)", "boolean", "bool"): + return "bool" + if any( + t.startswith(p) + for p in ( + "int", + "tinyint", + "smallint", + "mediumint", + "bigint", + "integer", + "decimal", + "numeric", + "float", + "double", + "real", + ) + ): + return "number" + if t == "date": + return "date" + if t.startswith(("datetime", "timestamp")): + return "datetime" + if t.startswith("time"): + return "time" + if any( + t.startswith(p) + for p in ("char", "varchar", "text", "tinytext", "mediumtext", "longtext") + ) or t in ("character varying", "character", "citext", "enum", "set"): + return "text" + if t.startswith(("character varying", "character")): + return "text" + if any(t.startswith(p) for p in ("blob", "binary", "varbinary", "bytea")): + return "binary" + return "other" + + +@dataclass +class ForeignKey: + column: str + table: str + ref_column: str + + +@dataclass +class Table: + name: str + columns: list[Column] + primary_key: list[str] + foreign_keys: list[ForeignKey] = field(default_factory=list) + + def column(self, name: str) -> Optional[Column]: + for c in self.columns: + if c.name == name: + return c + return None + + @property + def column_names(self) -> list[str]: + return [c.name for c in self.columns] + + +@dataclass +class Schema: + dialect: str + database: str + tables: dict[str, Table] + #: Views the read account sees, by name (they are read, never written). + views: list[str] = field(default_factory=list) + + def to_json(self) -> dict[str, Any]: + return { + "dialect": self.dialect, + "database": self.database, + "views": list(self.views), + "tables": {n: asdict(t) for n, t in self.tables.items()}, + } + + @classmethod + def from_json(cls, data: Mapping[str, Any]) -> "Schema": + tables = {} + for name, t in dict(data["tables"]).items(): + tables[name] = Table( + name=t["name"], + columns=[Column(**c) for c in t["columns"]], + primary_key=list(t["primary_key"]), + foreign_keys=[ForeignKey(**f) for f in t.get("foreign_keys", [])], + ) + return cls( + str(data["dialect"]), + str(data["database"]), + tables, + list(data.get("views", [])), + ) + + +def dialect_of(read: Mapping[str, Any]) -> str: + """``mysql`` or ``postgresql``, from the read's driver.""" + + driver = str(read.get("sql_driver") or "") + if driver in ("pymysql", "MySQLdb", "mysql.connector"): + return "mysql" + if driver in ("psycopg2", "psycopg"): + return "postgresql" + raise StoreError( + f"sql_driver {driver!r}: the generator reads MySQL/MariaDB (pymysql) " + "or PostgreSQL (psycopg2)" + ) + + +def connector(read: Mapping[str, Any]) -> Callable[[], Any]: + """A function that opens one read-only connection, as the contract will.""" + + driver = str(read.get("sql_driver") or "") + dialect_of(read) + module = importlib.import_module(driver) + args = dict(read.get("sql_connect_args") or {}) + env = read.get("sql_password_env") + if env: + if env not in os.environ: + raise StoreError(f"environment variable {env} (the read password) is not set") + args["password"] = os.environ[str(env)] + + def connect() -> Any: + from openadapt_flow.runtime.effects.sql import read_only_connection + + conn = module.connect(**args) + return read_only_connection(conn, driver) + + return connect + + +def _rows(cur: Any, sql: str, params: Sequence[Any] = ()) -> list[tuple[Any, ...]]: + cur.execute(sql, tuple(params)) if params else cur.execute(sql) + return [tuple(r) for r in cur.fetchall()] + + +def read_schema(connect: Callable[[], Any], dialect: str) -> Schema: + """Every base table the read account sees, with columns, keys and FKs.""" + + conn = connect() + try: + cur = conn.cursor() + if dialect == "mysql": + return _mysql_schema(cur) + if dialect == "postgresql": + return _pg_schema(cur) + raise StoreError(f"dialect {dialect!r} is not one of {DIALECTS}") + finally: + try: + conn.rollback() + finally: + conn.close() + + +def _mysql_schema(cur: Any) -> Schema: + (db,) = _rows(cur, "SELECT DATABASE()")[0] + if not db: + raise StoreError("the connection names no database (sql_connect_args.database)") + kinds = dict( + _rows( + cur, + "SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES " + "WHERE TABLE_SCHEMA = %s", + (db,), + ) + ) + cols = _rows( + cur, + "SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, DATA_TYPE, IS_NULLABLE, " + "COLUMN_DEFAULT, EXTRA, COLUMN_KEY FROM information_schema.COLUMNS " + "WHERE TABLE_SCHEMA = %s ORDER BY TABLE_NAME, ORDINAL_POSITION", + (db,), + ) + keys = _rows( + cur, + "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, " + "REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE " + "WHERE TABLE_SCHEMA = %s ORDER BY TABLE_NAME, CONSTRAINT_NAME, " + "ORDINAL_POSITION", + (db,), + ) + tables: dict[str, Table] = {} + for tname, cname, ctype, dtype, nullable, default, extra, _key in cols: + if kinds.get(tname) != "BASE TABLE": + continue + extra = str(extra or "").lower() + ctype = str(ctype).lower() + t = tables.setdefault(tname, Table(tname, [], [])) + t.columns.append( + Column( + name=str(cname), + data_type="tinyint(1)" if ctype.startswith("tinyint(1)") else str(dtype).lower(), + nullable=str(nullable).upper() == "YES", + default=None if default is None else str(default), + auto="auto_increment" in extra, + on_update="on update" in extra, + ) + ) + for tname, cname, cons, rtable, rcol in keys: + t = tables.get(tname) + if t is None: + continue + if cons == "PRIMARY": + t.primary_key.append(str(cname)) + elif rtable: + t.foreign_keys.append(ForeignKey(str(cname), str(rtable), str(rcol))) + views = sorted(n for n, k in kinds.items() if k == "VIEW") + return Schema("mysql", str(db), tables, views) + + +def _pg_schema(cur: Any) -> Schema: + (schema,) = _rows(cur, "SELECT current_schema()")[0] + (db,) = _rows(cur, "SELECT current_database()")[0] + kinds = dict( + _rows( + cur, + "SELECT table_name, table_type FROM information_schema.tables " + "WHERE table_schema = %s", + (schema,), + ) + ) + cols = _rows( + cur, + "SELECT table_name, column_name, data_type, is_nullable, column_default, " + "is_identity FROM information_schema.columns WHERE table_schema = %s " + "ORDER BY table_name, ordinal_position", + (schema,), + ) + tables: dict[str, Table] = {} + for tname, cname, dtype, nullable, default, identity in cols: + if kinds.get(tname) != "BASE TABLE": + continue + t = tables.setdefault(tname, Table(tname, [], [])) + default_s = None if default is None else str(default) + t.columns.append( + Column( + name=str(cname), + data_type=str(dtype).lower(), + nullable=str(nullable).upper() == "YES", + default=default_s, + auto=(default_s or "").startswith("nextval(") + or str(identity).upper() == "YES", + ) + ) + # pg_catalog: information_schema hides constraints of tables the account + # doesn't own, and the read account owns none. + cons = _rows( + cur, + "SELECT c.conrelid::regclass::text, c.contype, a.attname, " + "c.confrelid::regclass::text, fa.attname " + "FROM pg_constraint c " + "JOIN pg_namespace n ON n.oid = c.connamespace " + "CROSS JOIN LATERAL unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord) " + "JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum " + "LEFT JOIN pg_attribute fa ON fa.attrelid = c.confrelid " + "AND fa.attnum = c.confkey[k.ord] " + "WHERE n.nspname = %s AND c.contype IN ('p', 'f') " + "ORDER BY 1, c.conname, k.ord", + (schema,), + ) + for tname, ctype, cname, rtable, rcol in cons: + t = tables.get(str(tname).split(".")[-1].strip('"')) + if t is None: + continue + if ctype == "p": + t.primary_key.append(str(cname)) + elif rtable and rcol: + t.foreign_keys.append( + ForeignKey(str(cname), str(rtable).split(".")[-1].strip('"'), str(rcol)) + ) + views = sorted(n for n, k in kinds.items() if k == "VIEW") + return Schema("postgresql", str(db), tables, views) + + +def quote(dialect: str, name: str) -> str: + """A quoted identifier. Names with a quote character are refused.""" + + if "`" in name or '"' in name or "\x00" in name: + raise StoreError(f"identifier {name!r} holds a quote character") + return f"`{name}`" if dialect == "mysql" else f'"{name}"' + + +def cell_text(value: Any) -> Optional[str]: + """One cell as text, the way the generator compares values: ``None`` + stays ``None``; bytes are ``0x`` + hex; dates are ISO; a Decimal keeps its + stored digits.""" + + if value is None: + return None + if isinstance(value, (bytes, bytearray, memoryview)): + return "0x" + bytes(value).hex() + if isinstance(value, _dt.datetime): + return value.isoformat(sep=" ") + if isinstance(value, (_dt.date, _dt.time)): + return value.isoformat() + if isinstance(value, decimal.Decimal): + return format(value, "f") + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, float) and value.is_integer(): + return str(int(value)) + if isinstance(value, (dict, list)): + return json.dumps(value, sort_keys=True) + return str(value) + + +@dataclass +class TableRows: + #: Row key (the primary key's values joined by ``|``, or the whole row + #: for a table without one) -> column -> text. + rows: dict[str, dict[str, Optional[str]]] + #: Set when the table had more rows than the snapshot keeps. + count_only: Optional[int] = None + + +def row_key(table: Table, row: Mapping[str, Optional[str]]) -> str: + cols = table.primary_key or table.column_names + return "|".join("\x00" if row.get(c) is None else str(row.get(c)) for c in cols) + + +def snapshot( + connect: Callable[[], Any], + schema: Schema, + *, + max_rows: int = DEFAULT_MAX_ROWS, +) -> dict[str, TableRows]: + """Every table's rows as text, in one read-only transaction.""" + + conn = connect() + out: dict[str, TableRows] = {} + try: + cur = conn.cursor() + if schema.dialect == "mysql": + cur.execute("START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY") + else: + cur.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ") + for name in sorted(schema.tables): + table = schema.tables[name] + q = quote(schema.dialect, name) + (count,) = _rows(cur, f"SELECT COUNT(*) FROM {q}")[0] + if int(count) > max_rows: + out[name] = TableRows({}, count_only=int(count)) + continue + cols = table.column_names + cur.execute( + "SELECT " + + ", ".join(quote(schema.dialect, c) for c in cols) + + f" FROM {q}" + ) + rows: dict[str, dict[str, Optional[str]]] = {} + for raw in cur.fetchall(): + row = {c: cell_text(v) for c, v in zip(cols, raw)} + key = row_key(table, row) + while key in rows: # identical rows in a table without a key + key += "+" + rows[key] = row + out[name] = TableRows(rows) + finally: + try: + conn.rollback() + finally: + conn.close() + return out + + +def save_snapshot(path: Path, rows: Mapping[str, TableRows]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + data = {n: {"rows": t.rows, "count_only": t.count_only} for n, t in rows.items()} + tmp = path.with_suffix(".tmp") + tmp.write_text(json.dumps(data, sort_keys=True), encoding="utf-8") + os.chmod(tmp, 0o600) + tmp.replace(path) + + +def load_snapshot(path: Path) -> dict[str, TableRows]: + data = json.loads(path.read_text(encoding="utf-8")) + return { + n: TableRows(dict(t["rows"]), t.get("count_only")) for n, t in data.items() + } + + +@dataclass +class RowChange: + table: str + key: str + kind: str # added | removed | changed + before: Optional[dict[str, Optional[str]]] + after: Optional[dict[str, Optional[str]]] + + @property + def changed_columns(self) -> list[str]: + if self.before is None or self.after is None: + return [] + return sorted( + c + for c in set(self.before) | set(self.after) + if self.before.get(c) != self.after.get(c) + ) + + def to_json(self) -> dict[str, Any]: + return asdict(self) + + +@dataclass +class Diff: + changes: list[RowChange] + #: Tables too large to diff row by row whose count changed. + unread: dict[str, tuple[int, int]] = field(default_factory=dict) + + def tables(self) -> list[str]: + return sorted({c.table for c in self.changes} | set(self.unread)) + + def of(self, table: str, kind: Optional[str] = None) -> list[RowChange]: + return [ + c for c in self.changes if c.table == table and (kind is None or c.kind == kind) + ] + + def to_json(self) -> dict[str, Any]: + return { + "changes": [c.to_json() for c in self.changes], + "unread": {k: list(v) for k, v in self.unread.items()}, + } + + @classmethod + def from_json(cls, data: Mapping[str, Any]) -> "Diff": + return cls( + [RowChange(**c) for c in data.get("changes", [])], + {k: (int(v[0]), int(v[1])) for k, v in dict(data.get("unread", {})).items()}, + ) + + +def diff(before: Mapping[str, TableRows], after: Mapping[str, TableRows]) -> Diff: + """Rows added, removed or changed, table by table.""" + + changes: list[RowChange] = [] + unread: dict[str, tuple[int, int]] = {} + for name in sorted(set(before) | set(after)): + a, b = before.get(name), after.get(name) + if a is None or b is None: + unread[name] = (0 if a is None else len(a.rows), 0 if b is None else len(b.rows)) + continue + if a.count_only is not None or b.count_only is not None: + ca = a.count_only if a.count_only is not None else len(a.rows) + cb = b.count_only if b.count_only is not None else len(b.rows) + if ca != cb: + unread[name] = (ca, cb) + continue + for key in sorted(set(a.rows) | set(b.rows)): + ra, rb = a.rows.get(key), b.rows.get(key) + if ra == rb: + continue + kind = "added" if ra is None else "removed" if rb is None else "changed" + changes.append(RowChange(name, key, kind, ra, rb)) + return Diff(changes, unread) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py new file mode 100644 index 0000000000..c897bf84ca --- /dev/null +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -0,0 +1,1912 @@ +"""Propose ``openadapt.verify/v1`` contracts from a SQL schema and demonstrations. + + python -m openadapt_flow.generate.verify_contract --help + +A person setting up the verifier for an app used to write every contract by +hand, with 10 or 11 decisions per app. This generator writes them from: + +- the store's schema (``information_schema`` on MySQL, MariaDB or + PostgreSQL), read through the read-only account the contracts will use; +- a short description of each kind of write: its parameter names and one or + two example tasks (the ``kinds.yaml`` a setup agent is given); +- one demonstration write per example, which it snapshots before and after + to see which tables and columns the kind touches. + +For each kind it proposes one contract per field set the kind's tasks can +use (an update that may change the phone, the email or both gets a contract +for each), reading the whole database so an extra record anywhere is seen, +and a manifest that picks the contract for a task's values +(:func:`choose`). Whatever it can't decide alone becomes one numbered +question in ``QUESTIONS.md``; the goal is at most three per app. Answers go +in ``answers.yaml`` and the generator is run again. + +It never emits a contract that reads something that doesn't exist: every +contract is loaded and read once against the live store (a dry read) before +it is written out, and a contract that needs a database view gets it created +(``--create-views``, through an admin connection) or is refused with the +reason. See ``docs/GENERATE_CONTRACTS.md``. Nothing here calls a model. +""" + +from __future__ import annotations + +import argparse +import datetime as _dt +import hashlib +import itertools +import json +import os +import re +import sys +import tempfile +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Optional + +import yaml + +from openadapt_flow.generate import sql_store as store +from openadapt_flow.generate.demo_analysis import ( + _rank, + ColumnRole, + Demo, + Hop, + Location, + best, + column_roles, + is_identifier, + link_columns, + locate, + looks_like_log, + pick_anchor, + snake, +) +from openadapt_flow.generate.sql_store import RowChange, Schema, Table + +MANIFEST_SCHEMA = "openadapt.contract-manifest/v1" +#: Most contracts one kind gets (all subsets of up to six optional fields). +MAX_VARIANTS = 64 +#: Longest list a list parameter gets a contract for. +MAX_LIST = 10 +#: The recheck window proposed when the person hasn't answered. +DEFAULT_RECHECK_S = 300 + + +class GenerateError(RuntimeError): + """The generator can't go on; the message says why.""" + + +# -- inputs ------------------------------------------------------------------- + + +def load_yaml(path: Path) -> Any: + try: + return yaml.safe_load(Path(path).read_text(encoding="utf-8")) + except (OSError, yaml.YAMLError) as exc: + raise GenerateError(f"cannot read {path}: {exc}") from exc + + +def load_read(path: Path) -> dict[str, Any]: + """``read.yaml``: the read-only connection, as in a ``kind: sql`` + verifier (``sql_driver``, ``sql_connect_args``, ``sql_password_env``).""" + + data = load_yaml(path) + if not isinstance(data, dict) or not data.get("sql_driver"): + raise GenerateError(f"{path}: needs sql_driver and sql_connect_args") + if "password" in dict(data.get("sql_connect_args") or {}): + raise GenerateError( + f"{path}: put the password in an environment variable named by " + "sql_password_env, not in the file" + ) + return { + k: data[k] + for k in ("sql_driver", "sql_connect_args", "sql_password_env") + if k in data + } + + +@dataclass +class KindSpec: + kind: str + parameters: list[str] + description: str = "" + examples: list[dict[str, Any]] = field(default_factory=list) + + +def load_kinds(path: Path) -> list[KindSpec]: + data = load_yaml(path) or {} + kinds = data.get("kinds") if isinstance(data, dict) else None + if not isinstance(kinds, list) or not kinds: + raise GenerateError(f"{path}: needs a list under kinds:") + out = [] + for k in kinds: + if not isinstance(k, dict) or not k.get("kind") or not k.get("parameters"): + raise GenerateError(f"{path}: each kind needs kind: and parameters:") + out.append( + KindSpec( + str(k["kind"]), + [str(p) for p in k["parameters"]], + str(k.get("description") or ""), + [ + {"instruction": e.get("instruction", ""), "values": dict(e.get("values") or {})} + for e in k.get("examples") or [] + ], + ) + ) + return out + + +def load_demos(demo_dir: Path) -> list[Demo]: + demos = [] + for f in sorted(Path(demo_dir).glob("*.demo.json")): + d = json.loads(f.read_text(encoding="utf-8")) + after = store.load_snapshot(Path(demo_dir) / d["after_snapshot"]) + demos.append( + Demo( + kind=d["kind"], + values=dict(d["values"]), + diff=store.Diff.from_json(d["diff"]), + after=after, + started_at=d.get("started_at", ""), + finished_at=d.get("finished_at", ""), + name=f.name, + ) + ) + return demos + + +# -- questions ------------------------------------------------------------------ + + +@dataclass +class Question: + key: str + text: str + proposal: Any + options: list[str] = field(default_factory=list) + + +def _answer(answers: Mapping[str, Any], key: str) -> Any: + return (answers or {}).get(key) + + +# -- the plan for one kind -------------------------------------------------------- + + +@dataclass +class ChildGroup: + """Rows a write adds beside the main row that hold a parameter's value.""" + + table: str + link: Hop # child hop from the anchor + params: dict[str, Location] # param -> location relative to the child row + roles: dict[str, ColumnRole] + count: int = 1 + #: List parameter whose items these rows are, if any. + list_param: Optional[str] = None + changed_columns: list[str] = field(default_factory=list) + + +@dataclass +class SideSpec: + table: str + count: int + roles: dict[str, ColumnRole] + #: Used only when this parameter is not in the field set (the table holds + #: that parameter's row otherwise). + unless: Optional[str] = None + #: Added rows of other tables these rows hold the key of (for ownership). + owner: Optional[str] = None + + +@dataclass +class KindPlan: + kind: str + mode: str = "" # create | update + anchor: str = "" + params: list[str] = field(default_factory=list) + optional: list[str] = field(default_factory=list) + identity: list[str] = field(default_factory=list) + changes: list[str] = field(default_factory=list) + locations: dict[str, Location] = field(default_factory=dict) + #: dispatch value -> (value param -> location) + dispatch: Optional[str] = None + dispatch_locations: dict[str, dict[str, Location]] = field(default_factory=dict) + children: dict[str, ChildGroup] = field(default_factory=dict) # by param + lists: dict[str, ChildGroup] = field(default_factory=dict) + added_refs: dict[str, dict[str, Any]] = field(default_factory=dict) + sides: list[SideSpec] = field(default_factory=list) + logs: set[str] = field(default_factory=set) + bookkeeping: dict[str, set[str]] = field(default_factory=dict) + anchor_changed: set[str] = field(default_factory=set) + demos: list[Demo] = field(default_factory=list) + anchors: list[RowChange] = field(default_factory=list) + unresolved: dict[str, list[Location]] = field(default_factory=dict) + refused: list[str] = field(default_factory=list) + removed: dict[str, int] = field(default_factory=dict) + + +def _scalars(values: Mapping[str, Any]) -> dict[str, Any]: + return { + k: v for k, v in values.items() if v is not None and not isinstance(v, (list, dict)) + } + + +def _intersect(per_demo: list[list[Location]]) -> list[Location]: + if not per_demo: + return [] + common = {loc.signature(): loc for loc in per_demo[0]} + for locs in per_demo[1:]: + sigs = {loc.signature() for loc in locs} + common = {s: loc for s, loc in common.items() if s in sigs} + return list(common.values()) + + +def _as_change(table: str, row: Mapping[str, Optional[str]]) -> RowChange: + return RowChange(table, "", "added", None, dict(row)) + + +def analyze( + schema: Schema, + spec: KindSpec, + demos: Sequence[Demo], + actor_values: Mapping[str, str], +) -> KindPlan: + plan = KindPlan(spec.kind, params=list(spec.parameters)) + mine = [d for d in demos if d.kind == spec.kind and d.diff.changes] + if not mine: + plan.refused.append( + "no demonstration write of this kind (record one with " + "`verify_contract record`)" + ) + return plan + examples = [e["values"] for e in spec.examples] + [d.values for d in mine] + plan.optional = sorted( + p for p in spec.parameters if any(v.get(p) is None for v in examples) + ) + for d in mine: + anchor, mode, score = pick_anchor(schema, d, spec.parameters) + if anchor is None or score == 0: + plan.refused.append( + f"{d.name}: no row the write touched holds any of the task's values" + ) + return plan + if plan.anchor and (anchor.table != plan.anchor or mode != plan.mode): + plan.refused.append( + f"{d.name}: the main record is {anchor.table} ({mode}), but other " + f"demonstrations write {plan.anchor} ({plan.mode})" + ) + return plan + plan.anchor, plan.mode = anchor.table, mode + plan.demos.append(d) + plan.anchors.append(anchor) + lists = [p for p in spec.parameters if any(isinstance(d.values.get(p), list) for d in plan.demos)] + links = link_columns(schema, list(zip(plan.demos, plan.anchors))) + scalars = [p for p in spec.parameters if p not in lists] + + # Where each value is, in every demonstration that gives it. + found: dict[str, list[list[Location]]] = {p: [] for p in scalars} + roles_of: dict[str, set[str]] = {p: set() for p in scalars} + for d, a in zip(plan.demos, plan.anchors): + for p, v in _scalars(d.values).items(): + if p not in found: + continue + if plan.mode == "update": + change = locate(schema, d, a, v, changed_only=True, links=links) + ident = [ + loc + for loc in locate(schema, d, a, v, before=True) + if not loc.hops or loc.hops[0].kind == "fk" + ] + if ident and (not change or _rank(ident[0]) < _rank(change[0])): + found[p].append(ident) + roles_of[p].add("identity") + else: + found[p].append(change) + roles_of[p].add("change" if change else "missing") + else: + found[p].append(locate(schema, d, a, v, links=links)) + # A parameter whose value names a field (``field: status``). + for p in scalars: + values = [d.values.get(p) for d in plan.demos if d.values.get(p) is not None] + if ( + len(values) >= 2 + and all(is_identifier(v) for v in values) + and len(set(values)) == len(values) + and all(not locs for locs in found[p]) + and plan.dispatch is None + ): + plan.dispatch = p + for p in scalars: + if p == plan.dispatch: + continue + if plan.dispatch is not None: + by_value: dict[str, list[list[Location]]] = {} + for d, locs in zip( + [d for d in plan.demos if d.values.get(p) is not None], found[p] + ): + by_value.setdefault(str(d.values.get(plan.dispatch)), []).append(locs) + if len(by_value) > 1: + ok = True + for dv, per in by_value.items(): + loc, tied = best(_intersect(per)) + if loc is None: + plan.unresolved[f"{p} (when {plan.dispatch} is {dv})"] = tied + ok = False + else: + plan.dispatch_locations.setdefault(dv, {})[p] = loc + if ok: + plan.changes.append(p) + continue + loc, tied = best(_intersect(found[p])) + if loc is None: + if not found[p]: + continue # never given: no contract uses it + plan.unresolved[p] = tied + continue + plan.locations[p] = loc + if plan.mode == "update": + (plan.identity if "identity" in roles_of[p] else plan.changes).append(p) + # A column one value is already in is not another value's place. + progress = True + while progress and plan.unresolved: + progress = False + claimed = { + (loc.hops[0].column if loc.hops else c) + for loc in plan.locations.values() + for c in loc.columns + } + for name, tied in list(plan.unresolved.items()): + left = [ + loc + for loc in tied + if (loc.hops[0].column if loc.hops else loc.columns[0]) not in claimed + ] + if len(left) == 1 and name in plan.params: + plan.locations[name] = left[0] + plan.unresolved.pop(name) + if plan.mode == "update" and name not in plan.identity: + plan.changes.append(name) + progress = True + if plan.mode == "update" and not plan.identity: + plan.refused.append( + "no parameter names the record before the write, so the update's " + "target can't be told apart from other records" + ) + _children(schema, plan, lists) + _sides(schema, plan, actor_values) + return plan + + +def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: + """Group child-row locations, and find each list parameter's rows.""" + + for p, loc in list(plan.locations.items()): + if not loc.hops or loc.hops[0].kind != "child": + continue + rel = Location(loc.hops[1:], loc.columns, loc.rule) + rows = [] + for d, a in zip(plan.demos, plan.anchors): + if d.values.get(p) is None: + continue + for ch in d.diff.of(loc.hops[0].table, "added"): + if ch.after is None: + continue + key = (a.after or {}).get(loc.hops[0].ref_column) + if ch.after.get(loc.hops[0].column) != key: + continue + if locate(schema, d, _as_change(ch.table, ch.after), d.values[p], max_hops=len(rel.hops)): + rows.append((d, ch.after)) + break + explained = {loc.hops[0].column, *rel.columns} + explained |= {h.column for h in rel.hops[:1]} + plan.children[p] = ChildGroup( + loc.hops[0].table, + loc.hops[0], + {p: rel}, + column_roles(schema, loc.hops[0].table, rows, explained, {}), + 1, + ) + for p in lists: + per_key: dict[str, list[list[Location]]] = {} + table = link = None + rows = [] + for d, a in zip(plan.demos, plan.anchors): + items = d.values.get(p) + if not isinstance(items, list): + continue + key = (a.after or {}).get(schema.tables[plan.anchor].primary_key[0]) if schema.tables[plan.anchor].primary_key else None + children = [ + ch + for ch in d.diff.changes + if ch.kind == "added" + and ch.after is not None + and ch.table != plan.anchor + and key is not None + and key in ch.after.values() + ] + used: set[int] = set() + for item in items: + if not isinstance(item, dict): + plan.refused.append(f"{p}: list items must be mappings") + return + for i, ch in enumerate(children): + if i in used: + continue + locs = { + k: locate(schema, d, _as_change(ch.table, ch.after), v, max_hops=1) + for k, v in item.items() + } + if all(locs.values()): + used.add(i) + link_col = next( + c for c, v in ch.after.items() if v == key + ) + table = ch.table + link = Hop( + "child", + ch.table, + link_col, + schema.tables[plan.anchor].primary_key[0], + ) + for k, ls in locs.items(): + per_key.setdefault(k, []).append(ls) + rows.append((d, ch.after)) + break + else: + plan.unresolved[f"{p} item {item}"] = [] + if table is None or link is None: + plan.unresolved.setdefault(p, []) + continue + locs = {} + for k, per in per_key.items(): + loc, tied = best(_intersect(per)) + if loc is None: + plan.unresolved[f"{p}.{k}"] = tied + return + locs[k] = loc + explained = {link.column} | {c for loc in locs.values() for c in loc.columns} + explained |= {loc.hops[0].column for loc in locs.values() if loc.hops} + plan.lists[p] = ChildGroup( + table, link, locs, column_roles(schema, table, rows, explained, {}), list_param=p + ) + + +def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> None: + """Classify every other row the demonstrations touched.""" + + child_tables = {g.table: p for p, g in plan.children.items()} + list_tables = {g.table for g in plan.lists.values()} + pure: dict[str, list[list[tuple[Demo, dict[str, Optional[str]]]]]] = {} + counts: dict[str, list[int]] = {} + for d, a in zip(plan.demos, plan.anchors): + added_keys: set[str] = set() + for ch in d.diff.changes: + if ch.kind == "added" and ch.after is not None: + t = schema.tables.get(ch.table) + if t is not None and len(t.primary_key) == 1: + v = ch.after.get(t.primary_key[0]) + if v is not None: + added_keys.add(v) + per_table: dict[str, list[dict[str, Optional[str]]]] = {} + for ch in d.diff.changes: + if ch is a or ch.table == plan.anchor and ch.key == a.key: + continue + if ch.kind == "removed": + if ch.table in child_tables or ch.table in list_tables: + continue + plan.removed[ch.table] = plan.removed.get(ch.table, 0) + 1 + continue + if ch.kind == "changed": + linked = ch.table in child_tables and a.after is not None and any( + v == (a.after or {}).get(plan_pk(schema, plan.anchor)) + for v in (ch.after or {}).values() + ) + if linked: + g = plan.children[child_tables[ch.table]] + g.changed_columns = sorted(set(g.changed_columns) | set(ch.changed_columns)) + elif ch.table == plan.anchor: + plan.bookkeeping.setdefault(ch.table, set()).update(ch.changed_columns) + else: + plan.bookkeeping.setdefault(ch.table, set()).update(ch.changed_columns) + continue + # added + if ch.table in list_tables: + continue + if ch.table in child_tables and _is_param_row(schema, plan, d, ch): + per_table.setdefault(ch.table, []) + counts.setdefault(ch.table, [0] * len(plan.demos)) + continue + if _is_ref_row(schema, plan, d, ch): + continue + per_table.setdefault(ch.table, []).append(ch.after or {}) + i = plan.demos.index(d) + for t, rows in per_table.items(): + counts.setdefault(t, [0] * len(plan.demos)) + counts[t][i] = max(len(rows), 1 if t in child_tables else 0) + pure.setdefault(t, []).append([(d, r) for r in rows]) + d.__dict__["_added_keys"] = added_keys + for t, cs in sorted(counts.items()): + rows = [r for group in pure.get(t, []) for r in group] + if ( + len(set(cs)) == 1 + and cs[0] > 0 + and not looks_like_log(t) + and rows + ): + links = [getattr(d, "_added_keys", set()) for d, _ in rows] + roles = column_roles(schema, t, rows, set(), actor_values, links) + plan.sides.append( + SideSpec(t, cs[0], roles, unless=child_tables.get(t)) + ) + elif rows: + plan.logs.add(t) + if plan.mode == "update": + for a in plan.anchors: + plan.anchor_changed |= set(a.changed_columns) + explained = {c for loc in plan.locations.values() if not loc.hops for c in loc.columns} + explained |= {loc.hops[0].column for loc in plan.locations.values() if loc.hops and loc.hops[0].kind == "fk"} + for locs in plan.dispatch_locations.values(): + for loc in locs.values(): + explained |= set(loc.columns) if not loc.hops else {loc.hops[0].column} + extra = plan.anchor_changed - explained + if extra: + plan.bookkeeping.setdefault(plan.anchor, set()).update(extra) + + +def plan_pk(schema: Schema, table: str) -> str: + pk = schema.tables[table].primary_key + return pk[0] if len(pk) == 1 else "" + + +def _is_param_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: + for p, g in plan.children.items(): + if g.table != ch.table or d.values.get(p) is None or ch.after is None: + continue + rel = g.params[p] + if locate(schema, d, _as_change(ch.table, ch.after), d.values[p], max_hops=len(rel.hops)): + return True + return False + + +def _is_ref_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: + """A row the write added that a parameter's location points to (a new + email address row a link row names): declared by the value it holds.""" + + if ch.after is None: + return False + for p, g in plan.children.items(): + rel = g.params[p] + if not rel.hops or rel.hops[0].table != ch.table or d.values.get(p) is None: + continue + col = rel.columns[0] if len(rel.columns) == 1 else None + if col and ch.after.get(col) is not None and _rule_ok(ch.after.get(col), d.values[p], rel.rule): + plan.added_refs.setdefault(p, {"table": ch.table, "column": col, "rule": rel.rule}) + return True + return False + + +def _rule_ok(stored: Any, value: Any, rule: str) -> bool: + from openadapt_flow.generate.demo_analysis import rule_for + + return rule_for(stored, "text", value) == rule + + +# -- SQL -------------------------------------------------------------------------- + + +class SqlBuilder: + """SQL text for one dialect. Every value from a task is bound as a + parameter; table and column names come from the schema and are quoted.""" + + def __init__(self, schema: Schema) -> None: + self.schema = schema + self.d = schema.dialect + self.params: dict[str, dict[str, str]] = {} + self._n = itertools.count() + + def q(self, name: str) -> str: + return store.quote(self.d, name) + + def ph(self, param: str) -> str: + for k, v in self.params.items(): + if v == {"param": param}: + return f"%({k})s" + name = f"v{next(self._n)}" + self.params[name] = {"param": param} + return f"%({name})s" + + def text(self, alias: str, table: str, column: str) -> str: + col = self.schema.tables[table].column(column) + ref = f"{alias}.{self.q(column)}" + kind = col.kind if col else "other" + if self.d == "mysql": + if kind in ("binary", "other"): + return f"HEX({ref})" + return f"CAST({ref} AS CHAR)" + if kind == "binary": + return f"encode({ref}, 'hex')" + return f"CAST({ref} AS TEXT)" + + def display(self, alias: str, table: str, columns: Sequence[str], rule: str) -> str: + if rule == "minutes": + a, b = (f"{alias}.{self.q(c)}" for c in columns) + if self.d == "mysql": + return f"(TIMESTAMPDIFF(SECOND, {a}, {b}) / 60)" + return f"(EXTRACT(EPOCH FROM ({b} - {a})) / 60)" + if rule in ("number", "date", "datetime"): + return f"{alias}.{self.q(columns[0])}" + if len(columns) == 1: + return self.text(alias, table, columns[0]) + parts = [self.text(alias, table, c) for c in columns] + if self.d == "mysql": + return "CONCAT(" + ", ' ', ".join(parts) + ")" + return " || ' ' || ".join(parts) + + def path(self, alias: str, table: str, loc: Location, depth: int = 0) -> str: + if not loc.hops: + return self.display(alias, table, loc.columns, loc.rule) + hop, rest = loc.hops[0], Location(loc.hops[1:], loc.columns, loc.rule) + if hop.kind != "fk": + raise GenerateError("a child hop can't be read as one value") + sub = f"a{depth + 1}" + inner = self.path(sub, hop.table, rest, depth + 1) + return ( + f"(SELECT {inner} FROM {self.q(hop.table)} {sub} WHERE " + f"{self.same(f'{sub}.{self.q(hop.ref_column)}', f'{alias}.{self.q(hop.column)}', hop.declared)})" + ) + + def same(self, a: str, b: str, declared: bool = True) -> str: + if declared: + return f"{a} = {b}" + if self.d == "mysql": + return f"CAST({a} AS BINARY) = CAST({b} AS BINARY)" + return f"CAST({a} AS TEXT) = CAST({b} AS TEXT)" + + def flag(self, expr: str, rule: str, ph: str) -> str: + if self.d == "mysql": + if rule in ("number", "minutes"): + cond = ( + f"CAST({ph} AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND " + f"{expr} = CAST({ph} AS DECIMAL(65,30))" + ) + elif rule == "date": + cond = f"{expr} = CAST({ph} AS DATE)" + else: + cond = f"{expr} = CAST({ph} AS DATETIME)" + else: + if rule in ("number", "minutes"): + cond = f"CAST({expr} AS numeric) = CAST({ph} AS numeric)" + elif rule == "date": + cond = f"{expr} = CAST({ph} AS date)" + else: + cond = f"{expr} = CAST({ph} AS timestamp)" + return f"CASE WHEN {cond} THEN '1' ELSE '0' END" + + def literal(self, text: str) -> str: + if "\\" in text or "\x00" in text: + raise GenerateError("a constant holds a backslash or NUL; not pinned") + return "'" + text.replace("'", "''") + "'" + + def pin(self, alias: str, table: str, column: str, value: Optional[str]) -> str: + """A flag: this column holds the demonstrated constant (or no value).""" + + col = self.schema.tables[table].column(column) + ref = f"{alias}.{self.q(column)}" + if value is None: + return f"CASE WHEN {ref} IS NULL THEN '1' ELSE '0' END" + kind = col.kind if col else "text" + if kind in ("number", "bool"): + cast = "DECIMAL(65,30)" if self.d == "mysql" else "numeric" + lhs = ref if self.d == "mysql" else f"CAST({ref} AS numeric)" + if kind == "bool" and self.d != "mysql": + lhs = f"CAST(CAST({ref} AS integer) AS numeric)" + value = "1" if value in ("1", "true", "True") else "0" + return f"CASE WHEN {lhs} = CAST({self.literal(value)} AS {cast}) THEN '1' ELSE '0' END" + if kind == "date": + return f"CASE WHEN {ref} = CAST({self.literal(value)} AS DATE) THEN '1' ELSE '0' END" + return f"CASE WHEN {self.text(alias, table, column)} = {self.literal(value)} THEN '1' ELSE '0' END" + + def empty(self, alias: str, table: str, column: str) -> str: + col = self.schema.tables[table].column(column) + ref = f"{alias}.{self.q(column)}" + if col is not None and col.kind == "text": + return f"CASE WHEN {ref} IS NULL OR {ref} = '' THEN '1' ELSE '0' END" + return f"CASE WHEN {ref} IS NULL THEN '1' ELSE '0' END" + + def copied(self, alias: str, detail: Mapping[str, Any], table: str, column: str) -> str: + src = ( + f"(SELECT s.{self.q(detail['column'])} FROM {self.q(detail['table'])} s " + f"WHERE s.{self.q(detail['ref_column'])} = {alias}.{self.q(detail['via'])})" + ) + ref = f"{alias}.{self.q(column)}" + return f"CASE WHEN {ref} = {src} THEN '1' ELSE '0' END" + + def key(self, alias: str, table: str) -> Optional[str]: + pk = self.schema.tables[table].primary_key + if not pk: + return None + if len(pk) == 1: + return self.text(alias, table, pk[0]) + parts = [self.text(alias, table, c) for c in pk] + if self.d == "mysql": + return "CONCAT_WS('|', " + ", ".join(parts) + ")" + return " || '|' || ".join(parts) + + +def _alias(name: str) -> str: + out = re.sub(r"[^A-Za-z0-9_]", "_", name) + return out[:60] + + +@dataclass +class Surface: + name: str + table: str + columns: list[tuple[str, str]] # (alias, expr) + alias: str = "t" + + def sql(self, b: SqlBuilder) -> str: + cols = ", ".join(f"{expr} AS {b.q(a)}" for a, expr in self.columns) + return f"SELECT {cols} FROM {b.q(self.table)} {self.alias}" + + +def _base_columns(b: SqlBuilder, table: str, alias: str = "t") -> list[tuple[str, str]]: + t = b.schema.tables[table] + cols: list[tuple[str, str]] = [] + key = b.key(alias, table) + if key is not None: + cols.append(("id", key)) + for c in t.columns: + if c.name == "id" and key is not None: + continue + cols.append((c.name, b.text(alias, table, c.name))) + return cols + + +# -- one contract ------------------------------------------------------------------ + + +@dataclass +class Variant: + kind: str + fields: list[str] + lists: dict[str, int] = field(default_factory=dict) + dispatch_value: Optional[str] = None + files: dict[str, str] = field(default_factory=dict) # create | found | update -> path + sample: dict[str, Any] = field(default_factory=dict) + + @property + def slug(self) -> str: + parts = sorted(self.fields) + [f"{k}x{n}" for k, n in sorted(self.lists.items())] + if self.dispatch_value is not None: + parts.insert(0, f"{self.dispatch_value}") + text = "+".join(parts) + digest = hashlib.sha256(text.encode()).hexdigest()[:8] + short = re.sub(r"[^A-Za-z0-9_+]", "_", text)[:80] + return f"{short}.{digest}" + + +@dataclass +class Settings: + read: dict[str, Any] + actor_params: dict[str, str] + recheck_after_s: Optional[float] + environment_id: str + max_window_s: int = 900 + #: Tables a view replaces in surface reads (the SQL guard refuses the + #: table's name): table -> view name. + views: dict[str, str] = field(default_factory=dict) + + +def _value_expr(param: str, rule: str) -> dict[str, Any]: + out: dict[str, Any] = {"param": param} + if rule in ("email", "phone"): + out["kind"] = rule + return out + + +def build_contract( + schema: Schema, + plan: KindPlan, + variant: Variant, + flavor: str, + settings: Settings, + bookkeeping: Mapping[str, set[str]], + logs: set[str], +) -> dict[str, Any]: + """One ``openadapt.verify/v1`` document for one field set.""" + + b = SqlBuilder(schema) + fields = set(variant.fields) + anchor = plan.anchor + surfaces: dict[str, Surface] = {} + effects: list[dict[str, Any]] = [] + target_fields: set[str] = set() + named_tables: set[str] = {anchor} + + # The main record, with every column and the values its parameters reach. + main = Surface("record", settings.views.get(anchor, anchor), _base_columns(b, anchor)) + match: dict[str, Any] = {} + locations = dict(plan.locations) + if variant.dispatch_value is not None: + locations.update(plan.dispatch_locations.get(variant.dispatch_value, {})) + identity_params = [] + for p in sorted(fields): + loc = locations.get(p) + if loc is None or (loc.hops and loc.hops[0].kind == "child"): + continue + identity_params.append(p) + key = _place(b, main, anchor, p, loc) + if loc.rule in ("number", "date", "datetime", "minutes"): + match[key] = {"literal": "1"} + else: + match[key] = _value_expr(p, loc.rule) + if plan.mode == "update" and p in plan.changes: + target_fields.add(key) + target_fields.update(loc.columns if not loc.hops else [loc.hops[0].column]) + if plan.mode == "create" and flavor == "create": + roles = _anchor_roles(schema, plan, variant, settings.actor_params) + for col, role in sorted(roles.items()): + _pin(b, main, anchor, col, role, match, settings.actor_params) + for p in plan.optional: + if p in fields or p in variant.lists: + continue + loc = plan.locations.get(p) + if loc is None: + continue + if not loc.hops: + for c in loc.columns: + main.columns.append((f"empty__{c}", b.empty("t", anchor, c))) + match[f"empty__{c}"] = {"literal": "1"} + elif loc.hops[0].kind == "fk": + c = loc.hops[0].column + main.columns.append((f"empty__{c}", b.empty("t", anchor, c))) + match[f"empty__{c}"] = {"literal": "1"} + effect: dict[str, Any] = { + "kind": "record_written", + "probe": "surface=record|the main record holds the task's values", + "match": match, + "expected_count": 1, + } + if plan.mode == "create" and flavor == "create": + effect["count_new_only"] = True + effects.append(effect) + surfaces["record"] = main + + # Rows beside the main record that hold a value (a note, a custom field, + # an email address link), and list items (invoice lines). + if flavor != "found": + groups = [(p, g) for p, g in plan.children.items() if p in fields] + groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] + for p, g in groups: + name = f"rows:{g.table}" + sname = _alias(name) + if sname not in surfaces: + cols = _base_columns(b, g.table) + link = g.link + # The main record's values, seen from the child row. + for ip in identity_params: + loc = locations[ip] + up = Location( + (Hop("fk", anchor, link.column, link.ref_column, True),) + loc.hops, + loc.columns, + loc.rule, + ) + expr = b.path("t", g.table, up) + if loc.rule in ("number", "date", "datetime", "minutes"): + expr = b.flag(expr, loc.rule, b.ph(ip)) + cols.append((f"main__{_alias(ip)}", expr)) + surfaces[sname] = Surface(sname, settings.views.get(g.table, g.table), cols) + named_tables.add(g.table) + surf = surfaces[sname] + items = range(variant.lists[p]) if g.list_param else [None] + for i in items: + m: dict[str, Any] = {} + for ip in identity_params: + loc = locations[ip] + if loc.rule in ("number", "date", "datetime", "minutes"): + m[f"main__{_alias(ip)}"] = {"literal": "1"} + else: + m[f"main__{_alias(ip)}"] = _value_expr(ip, loc.rule) + for k, loc in g.params.items(): + pname = k if g.list_param is None else f"{p}.{i}.{k}" + key = _place(b, surf, g.table, pname, loc) + if loc.rule in ("number", "date", "datetime", "minutes"): + m[key] = {"literal": "1"} + else: + m[key] = _value_expr(pname, loc.rule) + for col, role in sorted(g.roles.items()): + if role.role == "link": + continue + _pin(b, surf, g.table, col, role, m, settings.actor_params) + effects.append( + { + "kind": "record_written", + "probe": f"surface={sname}|a row beside the main record holds {p}", + "match": m, + "expected_count": 1, + "count_new_only": True, + } + ) + if plan.mode == "update": + target_fields.update(g.changed_columns) + # A new row a link row names (a new email address). + for p, ref in plan.added_refs.items(): + if p not in fields: + continue + effects.append(_raw_effect(b, schema, surfaces, settings, ref["table"], {ref["column"]: _value_expr(p, ref["rule"])}, 1, {}, f"the new {ref['table']} row holds {p}")) + for side in plan.sides: + if side.unless is not None and side.unless in fields: + continue + m = {} + for col, role in sorted(side.roles.items()): + if role.role == "constant" and role.value is not None: + m[col] = {"literal": role.value} + elif role.role == "actor": + m[col] = {"param": role.detail["param"]} + effects.append( + _raw_effect(b, schema, surfaces, settings, side.table, m, side.count, side.roles, f"{side.count} row(s) the app adds to {side.table}") + ) + + verifier: dict[str, Any] = {"kind": "surfaces", "surfaces": {}} + for s in surfaces.values(): + sub: dict[str, Any] = {"kind": "sql", **settings.read, "sql_query": s.sql(b)} + used = {k: v for k, v in b.params.items() if f"%({k})s" in sub["sql_query"]} + if used: + sub["sql_query_params"] = used + verifier["surfaces"][s.name] = sub + # Main and child effects read named surfaces: no other read is judged + # against them, and the whole-database frame covers every other table. + ignore = sorted({c for cols in bookkeeping.values() for c in cols} & _all_columns(schema, named_tables)) + frame: dict[str, Any] = {} + if ignore: + frame["ignore_fields"] = ignore + if plan.mode == "create" or flavor == "found": + frame["target_fields"] = [] + else: + frame["target_fields"] = sorted(target_fields) + if schema.dialect == "mysql": + allow: list[dict[str, Any]] = [] + for t in sorted(logs): + if t in named_tables: + continue + allow.append({"table": t}) + for t in sorted(named_tables): + allow.append( + { + "table": t, + "mode": "skip", + "reason": "read in full, every column, by this contract's own surface", + } + ) + views = { + s.table: s.name for s in surfaces.values() if s.name.startswith("raw_") + } + db: dict[str, Any] = { + "dialect": "mysql", + "read": dict(settings.read), + "allow_tables": allow, + "views": views, + "settle_s": 1, + "settle_attempts": 5, + } + nrf = { + s.table: sorted(getattr(s, "_new_row_fields", [])) + for s in surfaces.values() + if s.name.startswith("raw_") and getattr(s, "_new_row_fields", None) + } + if nrf: + db["new_row_fields"] = nrf + ign = {t: sorted(c) for t, c in bookkeeping.items() if t not in named_tables and c} + if ign: + db["ignore_columns"] = ign + if settings.recheck_after_s: + db["recheck_after_s"] = settings.recheck_after_s + frame["database"] = db + else: + # PostgreSQL: frame.database reads MySQL and SQLite only, so every + # other table is read as its own surface (the read-set frame). + for t in sorted(schema.tables): + if t in named_tables or t in logs: + continue + if f"raw_{_alias(t)}" in surfaces: + continue + s = Surface(f"raw_{_alias(t)}", settings.views.get(t, t), _base_columns(b, t)) + surfaces[s.name] = s + verifier["surfaces"][s.name] = {"kind": "sql", **settings.read, "sql_query": s.sql(b)} + doc: dict[str, Any] = { + "schema_version": "openadapt.verify/v1", + "environment_id": settings.environment_id, + "max_execution_window_s": settings.max_window_s, + "verifier": verifier, + "effects": effects, + "frame": frame, + } + return doc + + +def _all_columns(schema: Schema, tables: set[str]) -> set[str]: + return {c.name for t in tables if t in schema.tables for c in schema.tables[t].columns} + + +def _place(b: SqlBuilder, surf: Surface, table: str, param: str, loc: Location) -> str: + """Add the column that holds ``param``'s value to ``surf``; its name.""" + + if not loc.hops and len(loc.columns) == 1 and loc.rule in ("exact", "email", "phone"): + return loc.columns[0] if loc.columns[0] != "id" else "id" + expr = b.path("t", table, loc) + alias = f"p__{_alias(param)}" + if loc.rule in ("number", "date", "datetime", "minutes"): + expr = b.flag(expr, loc.rule, b.ph(param)) + if alias not in {a for a, _ in surf.columns}: + surf.columns.append((alias, expr)) + return alias + + +def _pin( + b: SqlBuilder, + surf: Surface, + table: str, + col: str, + role: ColumnRole, + match: dict[str, Any], + actor_params: Mapping[str, str], +) -> None: + if role.role == "constant": + column = b.schema.tables[table].column(col) + if role.value is not None and column is not None and column.kind == "text": + match.setdefault(col, {"literal": role.value}) + return + alias = f"pin__{_alias(col)}" + surf.columns.append((alias, b.pin("t", table, col, role.value))) + match[alias] = {"literal": "1"} + elif role.role == "actor": + match.setdefault(col, {"param": role.detail["param"]}) + elif role.role == "copied": + alias = f"pin__{_alias(col)}" + surf.columns.append((alias, b.copied("t", role.detail, table, col))) + match[alias] = {"literal": "1"} + + +def _raw_effect( + b: SqlBuilder, + schema: Schema, + surfaces: dict[str, Surface], + settings: Settings, + table: str, + match: dict[str, Any], + count: int, + roles: Mapping[str, ColumnRole], + note: str, +) -> dict[str, Any]: + """An effect on a table's own read: rows the app adds beside the write. + + On MySQL the table is judged by the whole-database frame, which also + checks that every column these rows set beyond ``match`` is declared in + ``new_row_fields`` or holds its default. On PostgreSQL the surface reads + every column as text, so constants are pinned in ``match`` itself.""" + + name = f"raw_{_alias(table)}" + surf = surfaces.get(name) + if surf is None: + surf = Surface(name, settings.views.get(table, table), _base_columns(b, table)) + surfaces[name] = surf + t = schema.tables[table] + m = dict(match) + if schema.dialect == "mysql": + # Text and whole-number constants only: the snapshot's JSON form of + # a decimal or a date isn't the generator's text form. + for col, value in list(m.items()): + c = t.column(col) + lit = value.get("literal") if isinstance(value, dict) else None + if c is not None and lit is not None and not ( + c.kind == "text" or (c.kind in ("number", "bool") and re.fullmatch(r"-?\d+", lit)) + ): + m.pop(col) + nrf = set(getattr(surf, "_new_row_fields", set())) + for c in t.columns: + if c.name in m: + continue + role = roles.get(c.name) + if role is not None and role.role == "constant" and role.value is None: + continue + if role is None or role.role != "constant" or (role.detail or {}).get("is_default"): + if role is None or role.role in ("key", "timestamp", "link", "varies", "copied", "actor"): + nrf.add(c.name) + continue + nrf.add(c.name) + surf.__dict__["_new_row_fields"] = nrf + else: + for col, role in sorted(roles.items()): + if role.role == "constant" and col not in m: + if role.value is None: + alias = f"pin__{_alias(col)}" + if alias not in {a for a, _ in surf.columns}: + surf.columns.append((alias, b.pin("t", table, col, None))) + m[alias] = {"literal": "1"} + else: + m[col] = {"literal": role.value} + return { + "kind": "record_written", + "probe": f"surface={name}|{note}", + "match": m, + "expected_count": count, + "count_new_only": True, + } + + +def _anchor_roles( + schema: Schema, plan: KindPlan, variant: Variant, actor_params: Mapping[str, str] +) -> dict[str, ColumnRole]: + """The main row's columns no parameter explains, from the demonstrations + that give this field set (all of them when none gives exactly it).""" + + fields = set(variant.fields) | set(variant.lists) + if variant.dispatch_value is not None: + fields.add(plan.dispatch or "") + support = [ + (d, a) + for d, a in zip(plan.demos, plan.anchors) + if fields <= {k for k, v in d.values.items() if v is not None} + ] or list(zip(plan.demos, plan.anchors)) + explained: set[str] = set() + for loc in plan.locations.values(): + if not loc.hops: + explained |= set(loc.columns) + elif loc.hops[0].kind == "fk": + explained.add(loc.hops[0].column) + rows = [(d, a.after or {}) for d, a in support] + return column_roles(schema, plan.anchor, rows, explained, actor_params) + + +# -- variants ----------------------------------------------------------------------- + + +def variants(schema: Schema, plan: KindPlan, spec: KindSpec, answers: Mapping[str, Any]) -> list[Variant]: + out: list[Variant] = [] + lists = list(plan.lists) + if plan.mode == "update": + if plan.dispatch is not None: + values = dict(plan.dispatch_locations) + # By analogy: another column of the main table, written the way + # a demonstrated plain column is. + plain = [ + loc + for locs in values.values() + for loc in locs.values() + if not loc.hops and loc.rule == "exact" + ] + if plain: + vp = next(iter(next(iter(values.values())))) + for c in schema.tables[plan.anchor].columns: + for name in {c.name, _camel(c.name)}: + if name in values or c.name in schema.tables[plan.anchor].primary_key: + continue + if c.kind == "text": + values[name] = {vp: Location((), (c.name,), "exact")} + plan.dispatch_locations = values + for dv in sorted(values): + fields = sorted(set(plan.identity) | {plan.dispatch, *values[dv]}) + out.append(Variant(plan.kind, fields, dispatch_value=dv)) + return out + changes = [p for p in plan.changes if p in plan.locations] + for n in range(1, len(changes) + 1): + for combo in itertools.combinations(changes, n): + out.append(Variant(plan.kind, sorted(set(plan.identity) | set(combo)))) + return out[:MAX_VARIANTS] + located = set(plan.locations) + base_sets = [] + for d in plan.demos: + given = { + k for k, v in d.values.items() if v is not None and not isinstance(v, list) + } & (located | set(plan.children)) + base_sets.append(frozenset(given)) + optional = [p for p in plan.optional if p in located and p not in lists] + seen: set[frozenset[str]] = set() + sets: list[frozenset[str]] = [] + for base in base_sets: + drop = [p for p in optional if p in base] + for n in range(0, len(drop) + 1): + for combo in itertools.combinations(drop, n): + s = frozenset(base - set(combo)) + if s not in seen: + seen.add(s) + sets.append(s) + # A field the examples leave out sometimes, and that one demonstration + # gives, may be added to a set from another demonstration. + for s in list(sets): + for p in optional: + if p not in s: + t = frozenset(s | {p}) + if t not in seen: + seen.add(t) + sets.append(t) + sets = sets[:MAX_VARIANTS] + for s in sets: + if lists: + for n in range(1, MAX_LIST + 1): + out.append(Variant(plan.kind, sorted(s), lists={lists[0]: n})) + else: + out.append(Variant(plan.kind, sorted(s))) + return out + + +def _camel(name: str) -> str: + head, *rest = name.split("_") + return head + "".join(w.title() for w in rest) + + +# -- validation ------------------------------------------------------------------------ + + +def flatten(values: Mapping[str, Any]) -> dict[str, Any]: + """Task values as the contracts bind them: ``lines: [{qty: 4}]`` is + ``lines.0.qty: 4``; nulls are left out.""" + + out: dict[str, Any] = {} + for k, v in values.items(): + if v is None: + continue + if isinstance(v, list): + for i, item in enumerate(v): + if isinstance(item, dict): + for ik, iv in item.items(): + if iv is not None: + out[f"{k}.{i}.{ik}"] = iv + else: + out[f"{k}.{i}"] = item + else: + out[k] = v + return out + + +def sample_values(plan: KindPlan, variant: Variant) -> dict[str, Any]: + """Values to dry-read a contract with: the demonstration's own.""" + + out: dict[str, Any] = {} + for d in plan.demos: + for k, v in d.values.items(): + if v is not None and k not in out: + out[k] = v + if variant.dispatch_value is not None and plan.dispatch: + out[plan.dispatch] = variant.dispatch_value + for p, n in variant.lists.items(): + items = out.get(p) if isinstance(out.get(p), list) else [] + if items: + out[p] = [items[i % len(items)] for i in range(n)] + keep = set(variant.fields) | set(variant.lists) + return {k: v for k, v in out.items() if k in keep} + + +def dry_read(path: Path, params: Mapping[str, Any], mysql: bool) -> Optional[str]: + """Load the contract and read the live store once, as ``begin_write`` + would. None when it reads; else why not.""" + + from openadapt_flow.verifier import Contract, VerifierError + from openadapt_flow.verify_run import FRAME_DB_DETAIL, VerifyRunError, capture_baseline + + try: + contract = Contract.load(path) + except VerifierError as exc: + return f"does not load: {exc}" + with tempfile.TemporaryDirectory() as tmp: + try: + payload = capture_baseline(contract.config, params, Path(tmp) / "b.json") + except (VerifyRunError, ValueError, KeyError) as exc: + return f"dry read failed: {exc}" + state = payload.get("state", {}) + if not state.get("reachable"): + return "dry read: the store could not be read" + detail = state.get("detail") or {} + for name, sub in detail.items(): + if isinstance(sub, dict) and "reachable" in sub and not sub.get("reachable"): + if name == "openadapt:frame_audit": + continue + return f"dry read: surface {name!r} could not be read" + if mysql: + snap = detail.get(FRAME_DB_DETAIL) + if not isinstance(snap, dict): + return "dry read: the whole-database frame could not be read" + if snap.get("code_visible") is False: + return ( + "dry read: the read account can't see triggers (MySQL needs the " + "TRIGGER privilege on the database), so the frame can't be checked" + ) + return None + + +def check_read_only(connect: Any, dialect: str) -> list[str]: + """Why the read account is not read-only (empty when it is).""" + + problems: list[str] = [] + conn = connect() + try: + cur = conn.cursor() + if dialect == "mysql": + cur.execute("SHOW GRANTS") + for (grant,) in cur.fetchall(): + g = str(grant).upper() + privs = g.split(" ON ")[0].replace("GRANT ", "") + for word in ( + "ALL PRIVILEGES", + "INSERT", + "UPDATE", + "DELETE", + "CREATE", + "DROP", + "ALTER", + "INDEX", + "EXECUTE", + "FILE", + "SUPER", + "EVENT", + ): + if re.search(rf"\b{word}\b", privs) and not privs.startswith("USAGE"): + problems.append(f"the read account has {word}: {grant}") + else: + cur.execute( + "SELECT rolsuper, rolcreaterole, rolcreatedb FROM pg_roles " + "WHERE rolname = current_user" + ) + row = cur.fetchone() + if row and any(row): + problems.append("the read account is a superuser or can create roles or databases") + cur.execute( + "SELECT table_name FROM information_schema.role_table_grants " + "WHERE grantee = current_user AND privilege_type IN " + "('INSERT','UPDATE','DELETE','TRUNCATE') AND table_schema = current_schema() LIMIT 5" + ) + for (t,) in cur.fetchall(): + problems.append(f"the read account can write table {t}") + finally: + conn.rollback() + conn.close() + return problems + + +def guard_problem(sql: str) -> Optional[str]: + from openadapt_flow.runtime.effects.sql import assert_read_only_sql + + try: + assert_read_only_sql(sql) + except ValueError as exc: + return str(exc) + return None + + +def create_views( + admin_read: Mapping[str, Any], schema: Schema, tables: Sequence[str], reader: str +) -> dict[str, str]: + """Create one read-only view per table the SQL guard refuses by name, + in the same database, and grant the read account SELECT on it. Only run + with ``--create-views``; the connection is the admin one given.""" + + import importlib + + module = importlib.import_module(str(admin_read["sql_driver"])) + args = dict(admin_read.get("sql_connect_args") or {}) + env = admin_read.get("sql_password_env") + if env: + if env not in os.environ: + raise GenerateError(f"environment variable {env} (the admin password) is not set") + args["password"] = os.environ[str(env)] + conn = module.connect(**args) + made = {} + try: + cur = conn.cursor() + for t in tables: + view = f"oa_read_{t}" + q = store.quote(schema.dialect, t) + v = store.quote(schema.dialect, view) + cur.execute(f"CREATE OR REPLACE VIEW {v} AS SELECT * FROM {q}") + if schema.dialect == "mysql": + cur.execute(f"GRANT SELECT ON {v} TO {reader}") + else: + cur.execute(f"GRANT SELECT ON {v} TO {store.quote('postgresql', reader)}") + made[t] = view + conn.commit() + finally: + conn.close() + return made + + +# -- the whole run -------------------------------------------------------------------- + + +def propose( + *, + read: dict[str, Any], + kinds: list[KindSpec], + demo_dir: Path, + out: Path, + actor_params: Mapping[str, str], + answers: Mapping[str, Any], + environment_id: str = "", + admin_read: Optional[Mapping[str, Any]] = None, + create_views_flag: bool = False, + reader_account: str = "", + validate: bool = True, +) -> dict[str, Any]: + """Analyze, ask, emit and validate. Returns the report (also written).""" + + connect = store.connector(read) + dialect = store.dialect_of(read) + schema = store.read_schema(connect, dialect) + problems = check_read_only(connect, dialect) + if problems: + raise GenerateError( + "the read account is not read-only, so no contract is proposed: " + + "; ".join(problems) + ) + demos = load_demos(demo_dir) + env = environment_id or f"generated-{schema.database}" + env = re.sub(r"[^A-Za-z0-9._:-]", "-", env) + if len(env) < 8: + env = (env + "-environment")[:64] + plans = [analyze(schema, k, demos, actor_params) for k in kinds] + questions: list[Question] = [] + + # Q: bookkeeping, for the whole app at once. + logs = set().union(*(p.logs for p in plans)) if plans else set() + book: dict[str, set[str]] = {} + unchecked: dict[str, set[str]] = {} + for p in plans: + for t, cols in p.bookkeeping.items(): + book.setdefault(t, set()).update(cols) + if p.mode == "create" and p.demos: + for v in [Variant(p.kind, sorted(p.locations))]: + for col, role in _anchor_roles(schema, p, v, actor_params).items(): + if role.role == "varies": + unchecked.setdefault(p.anchor, set()).add(col) + ans = _answer(answers, "bookkeeping") + if isinstance(ans, dict): + logs = (logs | set(ans.get("logs_add", []))) - set(ans.get("logs_remove", [])) + for t, cols in dict(ans.get("columns_add", {})).items(): + book.setdefault(t, set()).update(cols) + for t, cols in dict(ans.get("columns_remove", {})).items(): + book[t] = book.get(t, set()) - set(cols) + if logs or book or unchecked: + lines = [] + if logs: + lines.append( + "Tables treated as the app's own logs (rows may be added, existing " + "rows must not change" + + (")" if dialect == "mysql" else "; on PostgreSQL they are not read at all)") + + ": " + + ", ".join(sorted(logs)) + ) + if book: + lines.append( + "Columns that change on their own and are not compared: " + + "; ".join(f"{t}: {', '.join(sorted(c))}" for t, c in sorted(book.items()) if c) + ) + if unchecked: + lines.append( + "Columns of a new record that differ between demonstrations and are " + "not checked: " + + "; ".join(f"{t}: {', '.join(sorted(c))}" for t, c in sorted(unchecked.items())) + ) + questions.append( + Question( + "bookkeeping", + "Is this the app's own bookkeeping, which a write may change without " + "a task asking? " + " ".join(lines) + + " Answer `bookkeeping: accept`, or list changes under " + "logs_add, logs_remove, columns_add, columns_remove.", + "accept", + ) + ) + # Q: the recheck window (MySQL: frame.database.recheck_after_s). + recheck: Optional[float] = None + if dialect == "mysql": + a = _answer(answers, "recheck_after_s") + recheck = float(a) if isinstance(a, (int, float)) and a > 0 else None + questions.append( + Question( + "recheck_after_s", + "How long after a verified write can the app still write on its own " + "(a background job, a delayed retry)? The database is read again that " + f"many seconds later and a late change refutes the write. Proposed: " + f"{DEFAULT_RECHECK_S}. Answer `recheck_after_s: `.", + DEFAULT_RECHECK_S, + ) + ) + # Q: per kind, a value the generator couldn't place. + for p in plans: + if not p.unresolved: + continue + chosen = dict(_answer(answers, f"locations.{p.kind}") or {}) + open_ = {} + for name, tied in p.unresolved.items(): + pick = chosen.get(name) + if isinstance(pick, int) and 0 <= pick < len(tied): + p.locations[name] = tied[pick] + if p.mode == "update" and name not in p.identity + p.changes: + p.changes.append(name) + else: + open_[name] = tied + p.unresolved = open_ + if open_: + parts = [] + for name, tied in open_.items(): + if tied: + opts = "; ".join(f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied)) + parts.append(f"`{name}` is in several places: {opts}") + else: + parts.append( + f"`{name}` was not found in any row the demonstrations wrote; " + "tasks that give it are stopped" + ) + questions.append( + Question( + f"locations.{p.kind}", + f"Kind `{p.kind}`: " + " ".join(parts) + + f" Answer `locations.{p.kind}: {{: }}` to pick one.", + None, + ) + ) + questions_open = [ + q for q in questions if _answer(answers, q.key) is None + ] + settings = Settings(read, dict(actor_params), recheck or (DEFAULT_RECHECK_S if dialect == "mysql" else None), env) + + # Tables whose name the SQL guard refuses: a view, or a refusal. + blocked = sorted( + t + for t in schema.tables + if guard_problem(f"SELECT * FROM {store.quote(dialect, t)} t") is not None + ) + needed = sorted( + {p.anchor for p in plans if p.anchor in blocked} + | {g.table for p in plans for g in list(p.children.values()) + list(p.lists.values()) if g.table in blocked} + ) + if needed: + existing = {t: f"oa_read_{t}" for t in needed if f"oa_read_{t}" in schema.views} + missing = [t for t in needed if t not in existing] + if missing and create_views_flag: + if not admin_read or not reader_account: + raise GenerateError("--create-views needs --admin-read and --reader-account") + existing.update(create_views(admin_read, schema, missing, reader_account)) + schema = store.read_schema(connect, dialect) + for t, v in list(existing.items()): + if v not in schema.views: + existing.pop(t) + settings.views = existing + for p in plans: + tables = {p.anchor} | {g.table for g in list(p.children.values()) + list(p.lists.values())} + for t in sorted(tables & set(blocked)): + if t not in existing: + p.refused.append( + f"the SQL guard refuses the table name {t!r} in a read; rerun " + "with --create-views --admin-read --reader-account " + " to read it through a view, or wait for the guard fix" + ) + + out.mkdir(parents=True, exist_ok=True) + manifest: dict[str, Any] = { + "schema_version": MANIFEST_SCHEMA, + "dialect": dialect, + "database": schema.database, + "actor_params": sorted(actor_params), + "questions_open": [q.key for q in questions_open], + "kinds": {}, + } + report: dict[str, Any] = { + "dialect": dialect, + "tables": len(schema.tables), + "questions": [ + {"number": i + 1, "key": q.key, "text": q.text, "proposal": q.proposal, "answered": _answer(answers, q.key) is not None} + for i, q in enumerate(questions) + ], + "kinds": {}, + } + for p, spec in zip(plans, kinds): + entry: dict[str, Any] = { + "mode": p.mode, + "main_table": p.anchor, + "parameters": p.params, + "identity": sorted(p.identity), + "lists": sorted(p.lists), + "dispatch": p.dispatch, + "variants": [], + } + rep: dict[str, Any] = { + "mode": p.mode, + "main_table": p.anchor, + "locations": {k: v.describe(p.anchor) for k, v in sorted(p.locations.items())}, + "children": {k: g.table for k, g in p.children.items()}, + "lists": {k: g.table for k, g in p.lists.items()}, + "side_rows": [{"table": s.table, "count": s.count, "unless": s.unless} for s in p.sides], + "logs": sorted(p.logs), + "refused": list(p.refused), + "unresolved": {k: [loc.describe(p.anchor) for loc in v] for k, v in sorted(p.unresolved.items())}, + "emitted": 0, + "rejected": [], + } + if p.removed: + rep["refused"].append( + "the demonstrations removed rows the generator can't explain: " + + ", ".join(f"{t} ({n})" for t, n in sorted(p.removed.items())) + ) + if p.refused or not p.mode: + manifest["kinds"][p.kind] = entry + report["kinds"][p.kind] = rep + continue + flavors = ["create", "found"] if p.mode == "create" else ["update"] + for v in variants(schema, p, spec, answers): + if any(f not in p.locations and f != p.dispatch and f not in p.children and f not in (p.dispatch_locations.get(v.dispatch_value or "", {})) for f in v.fields): + continue + ok = True + for flavor in flavors: + doc = build_contract(schema, p, v, flavor, settings, book, logs) + rel = Path("contracts") / p.kind / f"{v.slug}.{flavor}.yaml" + path = out / rel + path.parent.mkdir(parents=True, exist_ok=True) + header = ( + f"# Generated by openadapt_flow.generate.verify_contract: kind {p.kind}, " + f"{flavor}, fields {', '.join(v.fields + [f'{k}x{n}' for k, n in v.lists.items()])}.\n" + "# Proposed, not admitted: a person answers QUESTIONS.md first.\n" + ) + path.write_text(header + yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=100), encoding="utf-8") + why = None + for s in doc["verifier"]["surfaces"].values(): + why = why or guard_problem(s["sql_query"]) + if why is None and validate: + why = dry_read(path, flatten(sample_values(p, v)), dialect == "mysql") + if why is not None: + path.unlink() + rep["rejected"].append({"variant": v.slug, "flavor": flavor, "why": why}) + ok = False + break + v.files[flavor] = str(rel) + if ok: + rep["emitted"] += 1 + entry["variants"].append( + { + "fields": v.fields, + "lists": v.lists, + "dispatch_value": v.dispatch_value, + **{k: f for k, f in v.files.items()}, + } + ) + else: + for f in v.files.values(): + (out / f).unlink(missing_ok=True) + manifest["kinds"][p.kind] = entry + report["kinds"][p.kind] = rep + (out / "manifest.yaml").write_text(yaml.safe_dump(manifest, sort_keys=False, allow_unicode=True), encoding="utf-8") + (out / "report.json").write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8") + (out / "QUESTIONS.md").write_text(_questions_md(questions, answers, report), encoding="utf-8") + report["questions_open"] = [q.key for q in questions_open] + return report + + +def _questions_md(questions: Sequence[Question], answers: Mapping[str, Any], report: Mapping[str, Any]) -> str: + lines = ["# Questions for a person", ""] + if not questions: + lines.append("None: every decision followed from the schema and the demonstrations.") + for i, q in enumerate(questions, 1): + done = _answer(answers, q.key) + lines.append(f"{i}. {q.text}") + lines.append(f" - Answer key: `{q.key}`. Proposed: `{q.proposal}`.") + lines.append(f" - {'Answered: `' + json.dumps(done, ensure_ascii=False) + '`' if done is not None else 'Not answered yet.'}") + lines.append("") + lines.append("## Kinds") + lines.append("") + for kind, rep in report["kinds"].items(): + lines.append(f"- `{kind}` ({rep['mode'] or 'no contract'}, main table `{rep['main_table']}`): {rep['emitted']} contract(s).") + for r in rep["refused"]: + lines.append(f" - Refused: {r}") + for r in rep["rejected"][:5]: + lines.append(f" - Not emitted ({r['variant']}, {r['flavor']}): {r['why']}") + return "\n".join(lines) + "\n" + + +# -- at run time --------------------------------------------------------------------------- + + +def choose( + manifest_path: Path, + kind: str, + values: Mapping[str, Any], + extra_params: Optional[Mapping[str, Any]] = None, + *, + require_answers: bool = True, +) -> tuple[Any, dict[str, Any]]: + """The contract for one task, and the params to call ``begin_write`` with. + + The field set is the task's non-null values (a list counts by length). + For a create kind, the store is read first: when a record with the + task's values already exists the ``found`` contract applies (nothing may + change), else the ``create`` one. Raises :class:`GenerateError` when no + contract fits: the write is then stopped, never sent unchecked. + """ + + from openadapt_flow.verifier import Contract + + manifest_path = Path(manifest_path) + manifest = load_yaml(manifest_path) or {} + if manifest.get("schema_version") != MANIFEST_SCHEMA: + raise GenerateError(f"{manifest_path} is not a contract manifest") + if require_answers and manifest.get("questions_open"): + raise GenerateError( + "the manifest still has open questions: " + + ", ".join(manifest["questions_open"]) + + " (answer them in answers.yaml and run propose again)" + ) + entry = (manifest.get("kinds") or {}).get(kind) + if not entry or not entry.get("variants"): + raise GenerateError(f"no contract for kind {kind!r}") + given = {k for k, v in values.items() if v is not None and not isinstance(v, list)} + lists = {k: len(v) for k, v in values.items() if isinstance(v, list)} + dispatch = entry.get("dispatch") + for var in entry["variants"]: + if set(var["fields"]) != given or dict(var.get("lists") or {}) != lists: + continue + if dispatch and str(values.get(dispatch)) != str(var.get("dispatch_value")): + continue + params = {**flatten(values), **dict(extra_params or {})} + base = manifest_path.parent + if entry["mode"] == "update": + return Contract.load(base / var["update"]), params + found = Contract.load(base / var["found"]) + if _exists(found, params): + return found, params + return Contract.load(base / var["create"]), params + raise GenerateError( + f"no {kind} contract for the fields {sorted(given)}" + + (f" and lists {lists}" if lists else "") + + ": record a demonstration that gives them, or the task is stopped" + ) + + +def _exists(contract: Any, params: Mapping[str, Any]) -> bool: + """Does a record matching the found contract's main effect exist now?""" + + from openadapt_flow.runtime.effects.effect import record_matches + from openadapt_flow.verify_run import _build_verifier + + verifier = _build_verifier(contract.config, params) + state = verifier.capture_pre_state() + effect = contract.config.effects[0].resolve(params) + sub = (state.detail or {}).get("record", {}) + if not sub.get("reachable"): + raise GenerateError("the store could not be read to choose the contract") + selector = {k: v for k, v in effect.match.items()} + return any(record_matches(dict(r), selector) for r in sub.get("records", [])) + + +# -- CLI ------------------------------------------------------------------------------------ + + +def _now() -> str: + return _dt.datetime.now(_dt.timezone.utc).replace(tzinfo=None).isoformat(timespec="seconds") + + +def _parse_actor(items: Sequence[str]) -> dict[str, str]: + out = {} + for item in items or []: + if "=" not in item: + raise GenerateError(f"--actor-param {item!r}: use NAME=VALUE") + k, v = item.split("=", 1) + out[k.strip()] = v.strip() + return out + + +def main(argv: Optional[Sequence[str]] = None) -> int: + ap = argparse.ArgumentParser( + prog="python -m openadapt_flow.generate.verify_contract", + description=( + "Propose openadapt.verify/v1 contracts for an app's kinds of write, from " + "its SQL schema (MySQL, MariaDB or PostgreSQL) and one demonstration " + "write per example. Steps: `schema` (check the read account), then for " + "each example `snapshot` before the write and `record` after it, then " + "`propose`. Answer QUESTIONS.md in answers.yaml and run `propose` again. " + "Guide: packages/flow/docs/GENERATE_CONTRACTS.md. For a FHIR ensure-record " + "contract use openadapt_flow.generate.ensure_contract instead." + ), + ) + sub = ap.add_subparsers(dest="command", required=True) + s = sub.add_parser("schema", help="read the schema and check the account is read-only") + s.add_argument("--read", required=True, type=Path, help="read.yaml: sql_driver, sql_connect_args, sql_password_env") + s.add_argument("--out", type=Path, help="write schema.json here") + s = sub.add_parser("snapshot", help="snapshot every table before a demonstration write") + s.add_argument("--read", required=True, type=Path) + s.add_argument("--out", required=True, type=Path, help="e.g. work/before.json (holds row values: keep it private)") + s = sub.add_parser("record", help="snapshot again after the write and save the demonstration") + s.add_argument("--read", required=True, type=Path) + s.add_argument("--before", required=True, type=Path) + s.add_argument("--kind", required=True) + s.add_argument("--values", required=True, help="the task's values as JSON, or @file.json") + s.add_argument("--demos", required=True, type=Path, help="directory of demonstrations") + s.add_argument("--name", default="", help="file name stem (default: kind and a number)") + s = sub.add_parser("propose", help="analyze the demonstrations and write contracts") + s.add_argument("--read", required=True, type=Path) + s.add_argument("--kinds", required=True, type=Path, help="kinds.yaml: kinds, parameters, examples") + s.add_argument("--demos", required=True, type=Path) + s.add_argument("--out", required=True, type=Path) + s.add_argument("--answers", type=Path, help="answers.yaml with the person's answers") + s.add_argument("--actor-param", action="append", default=[], help="NAME=VALUE the caller passes for the acting user (e.g. bot_user_id=5); a column holding it is the actor") + s.add_argument("--environment-id", default="") + s.add_argument("--create-views", action="store_true", help="create a view for a table the SQL guard refuses by name (needs --admin-read)") + s.add_argument("--admin-read", type=Path, help="admin.yaml: a connection allowed to CREATE VIEW and GRANT") + s.add_argument("--reader-account", default="", help="the read account to GRANT SELECT on created views (MySQL: 'user'@'%%')") + s.add_argument("--no-validate", action="store_true", help="skip the dry read (tests only)") + args = ap.parse_args(argv) + try: + read = load_read(args.read) + connect = store.connector(read) + dialect = store.dialect_of(read) + if args.command == "schema": + schema = store.read_schema(connect, dialect) + problems = check_read_only(connect, dialect) + if args.out: + args.out.write_text(json.dumps(schema.to_json(), indent=1), encoding="utf-8") + print(f"{dialect} {schema.database}: {len(schema.tables)} tables, {len(schema.views)} views") + for p in problems: + print(f"NOT READ-ONLY: {p}") + return 1 if problems else 0 + if args.command == "snapshot": + schema = store.read_schema(connect, dialect) + rows = store.snapshot(connect, schema) + store.save_snapshot(args.out, rows) + (args.out.with_suffix(".meta.json")).write_text(json.dumps({"taken_at": _now()}), encoding="utf-8") + print(f"snapshot of {len(rows)} tables -> {args.out}") + return 0 + if args.command == "record": + schema = store.read_schema(connect, dialect) + values = args.values + if values.startswith("@"): + values = Path(values[1:]).read_text(encoding="utf-8") + values = json.loads(values) + before = store.load_snapshot(args.before) + meta = args.before.with_suffix(".meta.json") + started = json.loads(meta.read_text())["taken_at"] if meta.exists() else "" + after = store.snapshot(connect, schema) + d = store.diff(before, after) + args.demos.mkdir(parents=True, exist_ok=True) + n = len(list(args.demos.glob(f"{args.kind}-*.demo.json"))) + 1 + stem = args.name or f"{args.kind}-{n}" + snap = f"{stem}.after.json" + store.save_snapshot(args.demos / snap, after) + doc = { + "kind": args.kind, + "values": values, + "started_at": started, + "finished_at": _now(), + "after_snapshot": snap, + "diff": d.to_json(), + } + path = args.demos / f"{stem}.demo.json" + path.write_text(json.dumps(doc, indent=1, ensure_ascii=False), encoding="utf-8") + os.chmod(path, 0o600) + print(f"{path}: {len(d.changes)} row change(s) in {', '.join(d.tables()) or 'no table'}") + return 0 + answers = load_yaml(args.answers) if args.answers else {} + admin = load_read(args.admin_read) if args.admin_read else None + report = propose( + read=read, + kinds=load_kinds(args.kinds), + demo_dir=args.demos, + out=args.out, + actor_params=_parse_actor(args.actor_param), + answers=answers or {}, + environment_id=args.environment_id, + admin_read=admin, + create_views_flag=args.create_views, + reader_account=args.reader_account, + validate=not args.no_validate, + ) + print(f"questions: {len(report['questions'])} ({len(report['questions_open'])} open) -> {args.out / 'QUESTIONS.md'}") + for kind, rep in report["kinds"].items(): + status = "; ".join(rep["refused"]) if rep["refused"] else f"{rep['emitted']} contract(s)" + print(f" {kind}: {status}") + return 0 + except (GenerateError, store.StoreError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py new file mode 100644 index 0000000000..1e5b566bc1 --- /dev/null +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -0,0 +1,335 @@ +"""The contract generator on a real PostgreSQL: demonstrations in, contracts out. + +A throwaway PostgreSQL (``initdb``) holds a small issue tracker shaped like +Redmine: names behind keys with no declared foreign keys, a person's name in +two columns, notes in a journal table, custom fields in an +entity-attribute-value table, look-alike projects with the same issue +subject. The test records two demonstration writes per kind, lets the +generator propose contracts, then runs correct writes and planted faults +through ``begin_write`` and ``verify_write``. Every correct write must be +CONFIRMED and every fault stopped. Skipped when PostgreSQL isn't installed. +Records are made up. +""" + +from __future__ import annotations + +import json +import os +import shutil +import socket +import subprocess +import tempfile +import time +from pathlib import Path +from typing import Any, Callable + +import pytest +import yaml + +psycopg2 = pytest.importorskip("psycopg2") + +from openadapt_flow.generate import verify_contract as gen # noqa: E402 + +INITDB = shutil.which("initdb") or "/opt/homebrew/bin/initdb" +PG_CTL = shutil.which("pg_ctl") or "/opt/homebrew/bin/pg_ctl" + +SCHEMA = """ +CREATE TABLE projects (id serial PRIMARY KEY, name text NOT NULL); +CREATE TABLE users (id serial PRIMARY KEY, login text, firstname text, lastname text); +CREATE TABLE statuses (id serial PRIMARY KEY, name text); +CREATE TABLE issues ( + id serial PRIMARY KEY, project_id integer NOT NULL, subject text NOT NULL, + status_id integer NOT NULL, assigned_to_id integer, due_date date, + done_ratio integer NOT NULL DEFAULT 0, is_private boolean NOT NULL DEFAULT false, + author_id integer NOT NULL, created_on timestamp, updated_on timestamp, + lock_version integer NOT NULL DEFAULT 0); +CREATE TABLE journals (id serial PRIMARY KEY, journalized_id integer, + journalized_type text, user_id integer, notes text DEFAULT '', created_on timestamp); +CREATE TABLE journal_details (id serial PRIMARY KEY, journal_id integer, + prop_key text, old_value text, value text); +CREATE TABLE custom_values (id serial PRIMARY KEY, customized_type text, + customized_id integer, custom_field_id integer, value text); +CREATE TABLE settings (id serial PRIMARY KEY, name text, value text); +INSERT INTO projects (name) VALUES ('Billing Portal'), ('Billing Portal Legacy'), ('Help Desk'); +INSERT INTO users (login, firstname, lastname) VALUES ('bot', 'Ops', 'Bot'), + ('jpark', 'Jon', 'Park'), ('roconnor', 'Renée', 'O''Connor'), ('roconner', 'Renee', 'O''Conner'); +INSERT INTO statuses (name) VALUES ('New'), ('In Progress'), ('Resolved'); +INSERT INTO issues (project_id, subject, status_id, author_id, created_on, updated_on) VALUES + (1, 'Refund button missing', 1, 2, '2026-01-01', '2026-01-01'), + (2, 'Refund button missing', 1, 2, '2026-01-01', '2026-01-01'), + (3, 'Printer jams', 1, 2, '2026-01-01', '2026-01-01'), + (3, 'VPN drops', 2, 2, '2026-01-01', '2026-01-01'); +INSERT INTO settings (name, value) VALUES ('ui_theme', 'classic'); +CREATE ROLE oa_reader LOGIN PASSWORD 'reader-pw'; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO oa_reader; +""" + +BOT = 1 + + +def _free_port() -> int: + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + return int(s.getsockname()[1]) + + +@pytest.fixture(scope="module") +def pg() -> Any: + if not (os.path.exists(INITDB) and os.path.exists(PG_CTL)): + pytest.skip("PostgreSQL (initdb, pg_ctl) is not installed") + root = Path(tempfile.mkdtemp(prefix="oa-gen-pg-")) + port = _free_port() + env = {**os.environ, "LC_ALL": "C"} + subprocess.run([INITDB, "-D", str(root / "data"), "-U", "admin", "--auth=trust", "-E", "UTF8", "--locale=C"], + check=True, capture_output=True, env=env) # fmt: skip + subprocess.run( + [PG_CTL, "-D", str(root / "data"), "-l", str(root / "log"), "-w", "start", "-o", + f"-p {port} -c unix_socket_directories='' -c listen_addresses=127.0.0.1"], + check=True, capture_output=True, env=env, + ) # fmt: skip + try: + yield {"root": root, "port": port} + finally: + subprocess.run([PG_CTL, "-D", str(root / "data"), "-m", "immediate", "stop"], + capture_output=True, env=env) # fmt: skip + shutil.rmtree(root, ignore_errors=True) + + +class App: + """The tracker's own writes, as its API would make them.""" + + def __init__(self, port: int, db: str) -> None: + self.port, self.db = port, db + + def con(self) -> Any: + c = psycopg2.connect(host="127.0.0.1", port=self.port, user="admin", dbname=self.db) + c.autocommit = True + return c + + def q(self, sql: str, args: tuple = ()) -> list[tuple]: + with self.con() as c, c.cursor() as cur: + cur.execute(sql, args) + return cur.fetchall() if cur.description else [] + + def one(self, sql: str, args: tuple = ()) -> Any: + rows = self.q(sql, args) + assert len(rows) == 1, (sql, rows) + return rows[0][0] + + def user(self, name: str) -> int: + first, last = name.split(" ", 1) + return self.one("SELECT id FROM users WHERE firstname=%s AND lastname=%s", (first, last)) + + def create_issue(self, v: dict[str, Any], *, project_id: Any = None) -> int: + pid = project_id or self.one("SELECT id FROM projects WHERE name=%s", (v["project"],)) + sid = self.one("SELECT id FROM statuses WHERE name=%s", (v["status"],)) + uid = self.user(v["assignee"]) if v.get("assignee") else None + iid = self.one( + "INSERT INTO issues (project_id, subject, status_id, assigned_to_id, due_date, " + "author_id, created_on, updated_on) VALUES (%s,%s,%s,%s,%s,%s,now(),now()) RETURNING id", + (pid, v["subject"], sid, uid, v.get("due_date"), BOT), + ) + if v.get("customer_ref") is not None: + self.q("INSERT INTO custom_values (customized_type, customized_id, custom_field_id, value) " + "VALUES ('Issue', %s, 1, %s)", (iid, v["customer_ref"])) # fmt: skip + return iid + + def find(self, project: str, subject: str) -> int: + return self.one( + "SELECT i.id FROM issues i JOIN projects p ON p.id=i.project_id " + "WHERE p.name=%s AND i.subject=%s", (project, subject)) # fmt: skip + + def update_issue(self, iid: int, v: dict[str, Any], *, user: int = BOT) -> None: + sets, details = [], [] + if v.get("status") is not None: + sets.append(("status_id", self.one("SELECT id FROM statuses WHERE name=%s", (v["status"],)))) + if v.get("assignee") is not None: + sets.append(("assigned_to_id", self.user(v["assignee"]))) + if v.get("due_date") is not None: + sets.append(("due_date", v["due_date"])) + if v.get("done_ratio") is not None: + sets.append(("done_ratio", v["done_ratio"])) + for col, val in sets: + old = self.one(f"SELECT {col}::text FROM issues WHERE id=%s", (iid,)) + self.q(f"UPDATE issues SET {col}=%s WHERE id=%s", (val, iid)) + details.append((col, old, str(val))) + self.q("UPDATE issues SET updated_on=now(), lock_version=lock_version+1 WHERE id=%s", (iid,)) + jid = self.one( + "INSERT INTO journals (journalized_id, journalized_type, user_id, notes, created_on) " + "VALUES (%s,'Issue',%s,%s,now()) RETURNING id", (iid, user, v.get("notes") or "")) # fmt: skip + for col, old, new in details: + self.q("INSERT INTO journal_details (journal_id, prop_key, old_value, value) " + "VALUES (%s,%s,%s,%s)", (jid, col, old, new)) # fmt: skip + + +KINDS = { + "kinds": [ + { + "kind": "create_issue", + "parameters": ["project", "subject", "status", "assignee", "due_date", "customer_ref"], + "examples": [ + {"values": {"project": "Billing Portal", "subject": "Tax rounding", "status": "New", + "assignee": "Jon Park", "due_date": "2026-11-20", "customer_ref": None}}, + {"values": {"project": "Help Desk", "subject": "Onboard Zoë", "status": "New", + "assignee": "Renée O'Connor", "due_date": None, "customer_ref": "007341"}}, + ], + }, + { + "kind": "update_issue", + "parameters": ["project", "subject", "status", "assignee", "due_date", "done_ratio", "notes"], + "examples": [ + {"values": {"project": "Help Desk", "subject": "Printer jams", "status": "In Progress", + "assignee": "Jon Park", "due_date": None, "done_ratio": None, "notes": None}}, + {"values": {"project": "Billing Portal", "subject": "Refund button missing", + "status": None, "assignee": None, "due_date": None, "done_ratio": 30, + "notes": "Customer confirmed the refund."}}, + ], + }, + ] +} + + +@pytest.fixture(scope="module") +def generated(pg: Any) -> dict[str, Any]: + root, port = pg["root"], pg["port"] + admin = psycopg2.connect(host="127.0.0.1", port=port, user="admin", dbname="postgres") + admin.autocommit = True + admin.cursor().execute("CREATE DATABASE tracker") + admin.close() + app = App(port, "tracker") + with app.con() as c, c.cursor() as cur: + cur.execute(SCHEMA) + os.environ["OA_GEN_TEST_PW"] = "reader-pw" + work = root / "work" + work.mkdir() + read = {"sql_driver": "psycopg2", + "sql_connect_args": {"host": "127.0.0.1", "port": port, "user": "oa_reader", "dbname": "tracker"}, + "sql_password_env": "OA_GEN_TEST_PW"} # fmt: skip + (work / "read.yaml").write_text(yaml.safe_dump(read)) + (work / "kinds.yaml").write_text(yaml.safe_dump(KINDS, allow_unicode=True)) + common = ["--read", str(work / "read.yaml")] + assert gen.main(["schema", *common]) == 0 + + def demo(kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: + assert gen.main(["snapshot", *common, "--out", str(work / "before.json")]) == 0 + write() + assert gen.main(["record", *common, "--before", str(work / "before.json"), "--kind", kind, + "--values", json.dumps(values), "--demos", str(work / "demos")]) == 0 # fmt: skip + + for ex in KINDS["kinds"][0]["examples"]: + demo("create_issue", ex["values"], lambda v=ex["values"]: app.create_issue(v)) + for ex in KINDS["kinds"][1]["examples"]: + v = ex["values"] + demo("update_issue", v, lambda v=v: app.update_issue(app.find(v["project"], v["subject"]), v)) + argv = ["propose", *common, "--kinds", str(work / "kinds.yaml"), "--demos", str(work / "demos"), + "--out", str(work / "out"), "--actor-param", f"bot_user_id={BOT}"] # fmt: skip + assert gen.main(argv) == 0 + report = json.loads((work / "out" / "report.json").read_text()) + # The person accepts the proposals. + answers = {q["key"]: q["proposal"] for q in report["questions"]} + (work / "answers.yaml").write_text(yaml.safe_dump(answers)) + assert gen.main([*argv, "--answers", str(work / "answers.yaml")]) == 0 + if os.environ.get("OA_GEN_TEST_KEEP"): + shutil.copytree(work, os.environ["OA_GEN_TEST_KEEP"], dirs_exist_ok=True) + return {"app": app, "out": work / "out", "report": report, "read": read} + + +def _run(generated: dict[str, Any], kind: str, values: dict[str, Any], write: Callable[[], Any]) -> str: + from openadapt_flow.verifier import Actor, begin_write, verify_write + + contract, params = gen.choose(generated["out"] / "manifest.yaml", kind, values, {"bot_user_id": str(BOT)}) + with tempfile.TemporaryDirectory() as tmp: + pending = begin_write(contract, params, actor=Actor("agent", "ops-bot"), ledger=None, + write_key=f"t-{time.time_ns()}", workdir=tmp) # fmt: skip + write() + result = verify_write(pending) + return getattr(result.outcome, "value", str(result.outcome)) + " " + "; ".join(result.reasons)[:400] + + +def test_questions_and_contracts(generated: dict[str, Any]) -> None: + report = generated["report"] + keys = [q["key"] for q in report["questions"]] + assert len(keys) <= 3, keys + kinds = report["kinds"] + assert kinds["create_issue"]["mode"] == "create" + assert kinds["update_issue"]["mode"] == "update" + assert kinds["create_issue"]["emitted"] > 0, kinds["create_issue"] + # Four change fields the examples give (status, assignee, done ratio, + # notes): every non-empty subset gets a contract. No example gives a due + # date, so no contract changes one. + assert kinds["update_issue"]["emitted"] == 15, json.dumps(kinds["update_issue"], indent=1) + assert "journal_details" in kinds["update_issue"]["logs"] + + +def test_a_correct_create_is_confirmed(generated: dict[str, Any]) -> None: + app = generated["app"] + v = {"project": "Billing Portal", "subject": "Dark mode", "status": "New", + "assignee": "Renée O'Connor", "due_date": "2026-12-01", "customer_ref": "0099"} # fmt: skip + assert _run(generated, "create_issue", v, lambda: app.create_issue(v)).startswith("CONFIRMED") + + +def test_found_existing_with_no_write_is_confirmed(generated: dict[str, Any]) -> None: + app = generated["app"] + v = {"project": "Help Desk", "subject": "Make sure", "status": "New", + "assignee": "Jon Park", "due_date": None, "customer_ref": None} # fmt: skip + app.create_issue(v) + assert _run(generated, "create_issue", v, lambda: None).startswith("CONFIRMED") + + +def test_an_undemonstrated_update_combination_is_confirmed(generated: dict[str, Any]) -> None: + app = generated["app"] + v = {"project": "Help Desk", "subject": "VPN drops", "status": "Resolved", "assignee": None, + "due_date": None, "done_ratio": 80, "notes": "Fixed the tunnel."} # fmt: skip + iid = app.find("Help Desk", "VPN drops") + assert _run(generated, "update_issue", v, lambda: app.update_issue(iid, v)).startswith("CONFIRMED") + + +CREATE = {"project": "Billing Portal", "subject": "Export CSV", "status": "New", + "assignee": "Jon Park", "due_date": "2026-12-05", "customer_ref": "0042"} # fmt: skip +UPDATE = {"project": "Billing Portal", "subject": "Refund button missing", "status": "Resolved", + "assignee": None, "due_date": None, "done_ratio": None, "notes": "Done."} # fmt: skip + + +def _faults(app: App) -> dict[str, tuple[str, dict[str, Any], Callable[[], Any]]]: + def legacy() -> int: + return app.find("Billing Portal Legacy", "Refund button missing") + + def target() -> int: + return app.find("Billing Portal", "Refund button missing") + + return { + "wrong value": ("create_issue", CREATE, lambda: app.create_issue({**CREATE, "assignee": "Renee O'Conner"})), + "wrong project": ("create_issue", CREATE, lambda: app.create_issue(CREATE, project_id=2)), + "duplicate": ("create_issue", CREATE, lambda: (app.create_issue(CREATE), app.create_issue(CREATE))), + "nothing written": ("create_issue", CREATE, lambda: None), + "missing custom field": ("create_issue", CREATE, lambda: app.create_issue({**CREATE, "customer_ref": None})), + "extra issue": ("create_issue", CREATE, lambda: (app.create_issue(CREATE), app.create_issue({**CREATE, "subject": "Follow-up"}))), + "setting changed": ("create_issue", CREATE, lambda: (app.create_issue(CREATE), app.q("UPDATE settings SET value='dark'"))), + "look-alike target": ("update_issue", UPDATE, lambda: app.update_issue(legacy(), UPDATE)), + "both targets": ("update_issue", UPDATE, lambda: (app.update_issue(target(), UPDATE), app.update_issue(legacy(), {"status": "Resolved"}))), + "note missing": ("update_issue", UPDATE, lambda: app.update_issue(target(), {**UPDATE, "notes": None})), + "collateral field": ("update_issue", UPDATE, lambda: (app.update_issue(target(), UPDATE), app.q("UPDATE issues SET is_private=true WHERE id=%s", (target(),)))), + "collateral delete": ("update_issue", UPDATE, lambda: (app.update_issue(target(), UPDATE), app.q("DELETE FROM issues WHERE subject='Printer jams'"))), + "note by someone else": ("update_issue", UPDATE, lambda: app.update_issue(target(), UPDATE, user=2)), + } # fmt: skip + + +@pytest.mark.parametrize("name", list(_faults(App(0, "")).keys())) +def test_each_fault_is_stopped(generated: dict[str, Any], name: str) -> None: + app = generated["app"] + kind, values, write = _faults(app)[name] + with app.con() as c, c.cursor() as cur: # each fault starts from the same rows + cur.execute("BEGIN") + snapshot = {t: cur.execute(f"SELECT * FROM {t}") or cur.fetchall() + for t in ("issues", "journals", "journal_details", "custom_values", "settings")} # fmt: skip + cur.execute("COMMIT") + try: + outcome = _run(generated, kind, values, write) + print(f"FAULT {name}: {outcome}") + assert not outcome.startswith("CONFIRMED"), (name, outcome) + finally: + with app.con() as c, c.cursor() as cur: + for t, rows in snapshot.items(): + cur.execute(f"DELETE FROM {t}") + for r in rows: + cur.execute(f"INSERT INTO {t} VALUES ({', '.join(['%s'] * len(r))})", r) From 4c5b7c5b353d2ae2a937855a45718224626c16b4 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:30:30 -0400 Subject: [PATCH 02/94] WIP: generator trigger grant, demo and validation scripts --- .../generate_contracts/demo_dolibarr.py | 97 +++++++++++ .../benchmark/generate_contracts/demo_lib.py | 117 +++++++++++++ .../benchmark/generate_contracts/validate.py | 123 +++++++++++++ .../generate/verify_contract.py | 164 +++++++++++++----- 4 files changed, 458 insertions(+), 43 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/demo_dolibarr.py create mode 100644 packages/flow/benchmark/generate_contracts/demo_lib.py create mode 100644 packages/flow/benchmark/generate_contracts/validate.py diff --git a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py new file mode 100644 index 0000000000..4fd6a683ac --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py @@ -0,0 +1,97 @@ +"""Dolibarr (first study, instance 1): demonstrations, then the generator. + + FCG_INSTANCE=1 python benchmark/generate_contracts/demo_dolibarr.py WORKDIR + +Makes one demonstration write per example in ``setup_view.yaml`` through +Dolibarr's REST API as the bot (an example whose record already exists is +skipped: the right outcome is no write), then runs ``propose``. +""" + +from __future__ import annotations + +import calendar +import sys +from pathlib import Path + +import pymysql +import requests + +from demo_lib import demo, fixture, propose, setup_view, write_inputs + +APP = "dolibarr" + + +def main(work: Path) -> None: + fx = fixture(APP) + fx.reset() + bot = fx.bot() + ro = fx.read_access() + read = { + "sql_driver": "pymysql", + "sql_connect_args": {"host": ro["host"], "port": ro["port"], "user": ro["user"], "database": ro["database"]}, + "sql_password_env": "FCG_DOLIBARR_READER_PASSWORD", + } + write_inputs(work, APP, read) + db = pymysql.connect(host=ro["host"], port=ro["port"], user=ro["user"], password=ro["password"], database=ro["database"]) + + def q(sql: str, args: tuple = ()) -> list[tuple]: + with db.cursor() as cur: + cur.execute(sql, args) + return list(cur.fetchall()) + + def api(method: str, path: str, payload: object = None) -> object: + r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r.raise_for_status() + return r.json() + + def customer(name: str, town: str) -> int | None: + db.commit() + rows = q("SELECT rowid FROM llx_societe WHERE BINARY nom=%s AND BINARY town=%s", (name, town)) + return int(rows[0][0]) if len(rows) == 1 else None + + def country(label: str) -> int: + return int(q("SELECT rowid FROM llx_c_country WHERE label=%s", (label,))[0][0]) + + def product(ref: str) -> tuple[int, str, str]: + db.commit() + rid, price, tva = q("SELECT rowid, price, tva_tx FROM llx_product WHERE BINARY ref=%s", (ref,))[0] + return int(rid), str(price), str(tva) + + for k in setup_view(APP)["kinds"]: + for ex in k["examples"]: + v = ex["values"] + kind = k["kind"] + if kind == "create_customer": + if customer(v["name"], v["town"]) is not None: + continue + payload = {"name": v["name"], "address": v["address"], "zip": v["zip"], "town": v["town"], + "country_id": country(v["country"]), "phone": v["phone"], "email": v["email"], "client": 1} # fmt: skip + demo(work, kind, v, lambda p=payload: api("POST", "/thirdparties", p)) + elif kind == "create_product": + db.commit() + if q("SELECT 1 FROM llx_product WHERE BINARY ref=%s", (v["ref"],)): + continue + payload = {"ref": v["ref"], "label": v["label"], "type": 0, "price": v["price_excl_vat"], + "price_base_type": "HT", "tva_tx": v["vat_rate"], "status": 1, "status_buy": 1} # fmt: skip + demo(work, kind, v, lambda p=payload: api("POST", "/products", p)) + elif kind == "create_draft_invoice": + y, m, d = (int(x) for x in v["invoice_date"].split("-")) + lines = [] + for ln in v["lines"]: + pid, price, tva = product(ln["product_ref"]) + lines.append({"fk_product": pid, "qty": ln["qty"], "subprice": price, "tva_tx": tva, "product_type": 0}) + payload = {"socid": customer(v["customer"], v["town"]), "type": 0, + "date": calendar.timegm((y, m, d, 12, 0, 0, 0, 0, 0)), "lines": lines} # fmt: skip + demo(work, kind, v, lambda p=payload: api("POST", "/invoices", p)) + elif kind == "update_customer": + cid = customer(v["customer"], v["town"]) + change = {key[4:]: x for key, x in v.items() if key.startswith("new_") and x is not None} + demo(work, kind, v, lambda c=cid, p=change: api("PUT", f"/thirdparties/{c}", p)) + bot_id = q("SELECT rowid FROM llx_user WHERE login='fcg_bot'")[0][0] + report = propose(work, {"bot_user_id": str(bot_id)}) + print("questions:", len(report["questions"])) + fx.reset() + + +if __name__ == "__main__": + main(Path(sys.argv[1])) diff --git a/packages/flow/benchmark/generate_contracts/demo_lib.py b/packages/flow/benchmark/generate_contracts/demo_lib.py new file mode 100644 index 0000000000..f0f82bf224 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/demo_lib.py @@ -0,0 +1,117 @@ +"""Shared steps for running the contract generator on a first-study app. + +The generator is run the way a new customer would run it: it gets only the +app's read-only database account, the kinds and parameter names from the +app's ``setup_view.yaml``, and demonstration writes made here through the +app's public REST API (one per example). Nothing from the study's tasks, +oracle or faults reaches the generator. ``validate.py`` uses those later, +only to judge the contracts it wrote. + +Paths: ``FCG`` is the first study's folder (``apps//fixture.py``). +""" + +from __future__ import annotations + +import importlib.util +import json +import os +import sys +from pathlib import Path +from typing import Any, Callable + +import yaml + +from openadapt_flow.generate import verify_contract as gen + +FCG = Path( + os.environ.get( + "FCG_DIR", + str( + Path.home() + / "oa/worktrees/fcg-retest/evidence/first-contact-generalization-2026-10" + ), + ) +) + + +def load(name: str, path: Path) -> Any: + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type] + sys.modules[name] = mod + spec.loader.exec_module(mod) # type: ignore[union-attr] + return mod + + +def fixture(app: str) -> Any: + sys.path.insert(0, str(FCG / "apps" / app)) + return load(f"fcg_fixture_{app}", FCG / "apps" / app / "fixture.py") + + +def setup_view(app: str) -> dict[str, Any]: + return yaml.safe_load((FCG / "apps" / app / "setup_view.yaml").read_text(encoding="utf-8")) + + +def write_inputs(work: Path, app: str, read: dict[str, Any]) -> list[str]: + """``read.yaml`` and ``kinds.yaml`` in ``work``; the CLI's read flags.""" + + work.mkdir(parents=True, exist_ok=True) + (work / "read.yaml").write_text(yaml.safe_dump(read), encoding="utf-8") + view = setup_view(app) + kinds = { + "kinds": [ + { + "kind": k["kind"], + "description": k.get("description", ""), + "parameters": k["parameters"], + "examples": [{"values": e["values"]} for e in k.get("examples", [])], + } + for k in view["kinds"] + ] + } + (work / "kinds.yaml").write_text(yaml.safe_dump(kinds, allow_unicode=True), encoding="utf-8") + return ["--read", str(work / "read.yaml")] + + +def demo(work: Path, kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: + """Snapshot, make one demonstration write, record the diff.""" + + flags = ["--read", str(work / "read.yaml")] + assert gen.main(["snapshot", *flags, "--out", str(work / "before.json")]) == 0 + write() + assert ( + gen.main( + [ + "record", + *flags, + "--before", + str(work / "before.json"), + "--kind", + kind, + "--values", + json.dumps(values, ensure_ascii=False), + "--demos", + str(work / "demos"), + ] + ) + == 0 + ) + + +def propose(work: Path, actor: dict[str, str], extra: list[str] = ()) -> dict[str, Any]: + argv = [ + "propose", + "--read", + str(work / "read.yaml"), + "--kinds", + str(work / "kinds.yaml"), + "--demos", + str(work / "demos"), + "--out", + str(work / "out"), + *[f"--actor-param={k}={v}" for k, v in actor.items()], + *extra, + ] + if (work / "answers.yaml").exists(): + argv += ["--answers", str(work / "answers.yaml")] + gen.main(argv) + return json.loads((work / "out" / "report.json").read_text(encoding="utf-8")) diff --git a/packages/flow/benchmark/generate_contracts/validate.py b/packages/flow/benchmark/generate_contracts/validate.py new file mode 100644 index 0000000000..65f7eedab3 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/validate.py @@ -0,0 +1,123 @@ +"""Judge generated contracts on a first-study app: correct writes and faults. + + FCG_INSTANCE=1 python benchmark/generate_contracts/validate.py dolibarr WORKDIR [--only T01 F05] + +For every task of the study, the pack's reference correct write; for every +planted fault, the fault. Each case: reset the app, pick the contract with +``verify_contract.choose`` (from the task's values only), ``begin_write``, +write, ``verify_write``, and the study's oracle labels what happened. Results +append to ``WORKDIR/validate.jsonl``; a case already there is skipped. + +The tasks, faults and oracle are read here only, never by the generator. +""" + +from __future__ import annotations + +import argparse +import json +import os +import tempfile +import time +import traceback +from pathlib import Path +from typing import Any + +import yaml + +from demo_lib import FCG, fixture, load + +from openadapt_flow.generate import verify_contract as gen +from openadapt_flow.verifier import Actor, begin_write, verify_write + +LATE_WAIT_S = 150 + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("app") + ap.add_argument("work", type=Path) + ap.add_argument("--only", nargs="*") + ap.add_argument("--actor-id", required=True, help="the bot's id as the contracts bind it") + ap.add_argument("--actor-param", default="bot_user_id") + ap.add_argument("--no-faults", action="store_true") + ap.add_argument("--no-correct", action="store_true") + args = ap.parse_args() + os.environ.setdefault("OPENADAPT_LOCAL_SEAL", "off") + app_dir = FCG / "apps" / args.app + fx = fixture(args.app) + oracle = load(f"fcg_oracle_{args.app}", app_dir / "oracle.py") + faults = load(f"fcg_faults_{args.app}", app_dir / "faults" / "faults.py") + tasks = {t["id"]: t for t in yaml.safe_load((app_dir / "tasks.yaml").read_text())} + manifest = args.work / "out" / "manifest.yaml" + out = args.work / "validate.jsonl" + done = set() + if out.exists(): + done = {json.loads(x)["case"] for x in out.read_text().splitlines() if x.strip()} + cases: list[tuple[str, dict, Any, float]] = [] + if not args.no_correct: + for tid, task in tasks.items(): + cases.append((f"correct-{tid}", task, lambda t=task: faults.correct_write(t, fx.bot()), 0.0)) + if not args.no_faults: + for f in yaml.safe_load((app_dir / "faults" / "faults.yaml").read_text()): + task = tasks[f["task"]] + fn = getattr(faults, f["id"]) + late = 0.0 + if f.get("class") == "late_write" or f.get("after_snapshot_delay_s"): + late = max(LATE_WAIT_S, float(f.get("after_snapshot_delay_s") or 0)) + cases.append( + (f"fault-{f['id']}", task, + lambda fn=fn, t=task: fn(t, fx.bot(), fx.other_user(), fx.read_access()), late) + ) # fmt: skip + for case, task, act, late in cases: + short = case.split("-", 1)[1] + if case in done or (args.only and short not in args.only and task["id"] not in args.only): + continue + while os.getloadavg()[0] >= 35: + time.sleep(30) + rec: dict[str, Any] = {"case": case, "task": task["id"], "kind": task["kind"]} + fx.reset() + before = oracle.snapshot() + pending = None + try: + contract, params = gen.choose(manifest, task["kind"], task["values"], {args.actor_param: args.actor_id}) + rec["contract"] = contract.name + tmp = tempfile.mkdtemp() + pending = begin_write(contract, params, actor=Actor("agent", "generated-check"), + ledger=None, write_key=f"{case}-{time.time_ns()}", workdir=tmp) # fmt: skip + except Exception as exc: # noqa: BLE001 - a stop before the write + rec["verdict"] = {"outcome": "BLOCKED_AT_BEGIN", "reasons": [f"{type(exc).__name__}: {exc}"]} + try: + rec["claim"] = act() + except Exception as exc: # noqa: BLE001 + rec["claim"] = {"status": "not_done", "note": f"{type(exc).__name__}: {exc}"} + if pending is not None: + try: + r = verify_write(pending) + rec["verdict"] = { + "outcome": getattr(r.outcome, "value", str(r.outcome)), + "reasons": list(r.reasons), + "recheck_due_at": getattr(r, "recheck_due_at", None), + } + except Exception as exc: # noqa: BLE001 + rec["verdict"] = {"outcome": "VERIFIER_CRASH", "reasons": [f"{type(exc).__name__}: {exc}"], + "trace": traceback.format_exc()[-2000:]} # fmt: skip + if late: + time.sleep(late) + if hasattr(fx, "wait_late_writes"): + fx.wait_late_writes() + after = oracle.snapshot() + try: + rec["oracle"] = oracle.judge(task, before, after) + except Exception as exc: # noqa: BLE001 + rec["oracle"] = {"label": "oracle_error", "reasons": [str(exc)]} + with out.open("a", encoding="utf-8") as f: + f.write(json.dumps(rec, default=str, ensure_ascii=False) + "\n") + v = rec["verdict"] + print(f"{case} [{task['kind']}]: {v['outcome']} | oracle {rec['oracle'].get('label')} | " + f"{'; '.join(v.get('reasons', []))[:200]}", flush=True) # fmt: skip + fx.reset() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index c897bf84ca..caccdbf34a 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1307,7 +1307,27 @@ def dry_read(path: Path, params: Mapping[str, Any], mysql: bool) -> Optional[str if mysql: snap = detail.get(FRAME_DB_DETAIL) if not isinstance(snap, dict): - return "dry read: the whole-database frame could not be read" + from openadapt_flow.runtime.effects.db_snapshot import mac_key, take_snapshot + from openadapt_flow.verify_run import _audit_sql, _db_connect, _ref_columns + + database = contract.config.frame.database + try: + take_snapshot( + _db_connect(database.read), + database.dialect, + key=mac_key(b"0" * 32), + modes=database.modes(), + max_rows=database.max_rows, + extra_queries=_audit_sql(contract.config), + ignore=database.all_ignore_columns(), + ref_columns=_ref_columns(contract.config), + schemas=database.schemas, + links=database.recheck_after_s is not None, + ) + why = "no error on a second read" + except Exception as exc: # noqa: BLE001 - reported + why = f"{type(exc).__name__}: {exc}" + return f"dry read: the whole-database frame could not be read ({why})" if snap.get("code_visible") is False: return ( "dry read: the read account can't see triggers (MySQL needs the " @@ -1375,13 +1395,7 @@ def guard_problem(sql: str) -> Optional[str]: return None -def create_views( - admin_read: Mapping[str, Any], schema: Schema, tables: Sequence[str], reader: str -) -> dict[str, str]: - """Create one read-only view per table the SQL guard refuses by name, - in the same database, and grant the read account SELECT on it. Only run - with ``--create-views``; the connection is the admin one given.""" - +def _admin_connect(admin_read: Mapping[str, Any]) -> Any: import importlib module = importlib.import_module(str(admin_read["sql_driver"])) @@ -1391,7 +1405,51 @@ def create_views( if env not in os.environ: raise GenerateError(f"environment variable {env} (the admin password) is not set") args["password"] = os.environ[str(env)] - conn = module.connect(**args) + return module.connect(**args) + + +def grant_trigger(admin_read: Mapping[str, Any], schema: Schema, reader: str) -> str: + """MySQL shows a table's triggers only to an account with the TRIGGER + privilege, and the whole-database frame must see them (a trigger planted + during a write could otherwise pass). Run only with ``--grant-trigger``.""" + + conn = _admin_connect(admin_read) + try: + sql = f"GRANT TRIGGER ON {store.quote('mysql', schema.database)}.* TO {reader}" + conn.cursor().execute(sql) + conn.commit() + finally: + conn.close() + return sql + + +def _sees_triggers(connect: Any) -> bool: + """Does the account hold TRIGGER (or ALL) on the database? A database + with no triggers shows none either way, so the grant is what's checked.""" + + conn = connect() + try: + cur = conn.cursor() + cur.execute("SHOW GRANTS") + grants = [str(g[0]).upper() for g in cur.fetchall()] + finally: + conn.rollback() + conn.close() + for g in grants: + privs = g.split(" ON ")[0] + if "TRIGGER" in privs or "ALL PRIVILEGES" in privs: + return True + return False + + +def create_views( + admin_read: Mapping[str, Any], schema: Schema, tables: Sequence[str], reader: str +) -> dict[str, str]: + """Create one read-only view per table the SQL guard refuses by name, + in the same database, and grant the read account SELECT on it. Only run + with ``--create-views``; the connection is the admin one given.""" + + conn = _admin_connect(admin_read) made = {} try: cur = conn.cursor() @@ -1427,6 +1485,7 @@ def propose( create_views_flag: bool = False, reader_account: str = "", validate: bool = True, + grant_trigger_flag: bool = False, ) -> dict[str, Any]: """Analyze, ask, emit and validate. Returns the report (also written).""" @@ -1439,6 +1498,20 @@ def propose( "the read account is not read-only, so no contract is proposed: " + "; ".join(problems) ) + applied: list[str] = [] + if dialect == "mysql" and not _sees_triggers(connect): + if grant_trigger_flag: + if not admin_read or not reader_account: + raise GenerateError("--grant-trigger needs --admin-read and --reader-account") + applied.append(grant_trigger(admin_read, schema, reader_account)) + if not _sees_triggers(connect): + raise GenerateError( + "the read account can't see triggers, so the whole-database frame " + "would never be checked (every write would be UNCERTAIN). MySQL shows " + "triggers only to an account with the TRIGGER privilege. Rerun with " + "--grant-trigger --admin-read --reader-account , " + "or have the DBA run: GRANT TRIGGER ON .* TO " + ) demos = load_demos(demo_dir) env = environment_id or f"generated-{schema.database}" env = re.sub(r"[^A-Za-z0-9._:-]", "-", env) @@ -1447,6 +1520,41 @@ def propose( plans = [analyze(schema, k, demos, actor_params) for k in kinds] questions: list[Question] = [] + location_questions: list[Question] = [] + # Q: per kind, a value the generator couldn't place. + for p in plans: + if not p.unresolved: + continue + chosen = dict(_answer(answers, f"locations.{p.kind}") or {}) + open_ = {} + for name, tied in p.unresolved.items(): + pick = chosen.get(name) + if isinstance(pick, int) and 0 <= pick < len(tied): + p.locations[name] = tied[pick] + if p.mode == "update" and name not in p.identity + p.changes: + p.changes.append(name) + else: + open_[name] = tied + p.unresolved = open_ + if open_: + parts = [] + for name, tied in open_.items(): + if tied: + opts = "; ".join(f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied)) + parts.append(f"`{name}` is in several places: {opts}") + else: + parts.append( + f"`{name}` was not found in any row the demonstrations wrote; " + "tasks that give it are stopped" + ) + location_questions.append( + Question( + f"locations.{p.kind}", + f"Kind `{p.kind}`: " + " ".join(parts) + + f" Answer `locations.{p.kind}: {{: }}` to pick one.", + None, + ) + ) # Q: bookkeeping, for the whole app at once. logs = set().union(*(p.logs for p in plans)) if plans else set() book: dict[str, set[str]] = {} @@ -1512,40 +1620,7 @@ def propose( DEFAULT_RECHECK_S, ) ) - # Q: per kind, a value the generator couldn't place. - for p in plans: - if not p.unresolved: - continue - chosen = dict(_answer(answers, f"locations.{p.kind}") or {}) - open_ = {} - for name, tied in p.unresolved.items(): - pick = chosen.get(name) - if isinstance(pick, int) and 0 <= pick < len(tied): - p.locations[name] = tied[pick] - if p.mode == "update" and name not in p.identity + p.changes: - p.changes.append(name) - else: - open_[name] = tied - p.unresolved = open_ - if open_: - parts = [] - for name, tied in open_.items(): - if tied: - opts = "; ".join(f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied)) - parts.append(f"`{name}` is in several places: {opts}") - else: - parts.append( - f"`{name}` was not found in any row the demonstrations wrote; " - "tasks that give it are stopped" - ) - questions.append( - Question( - f"locations.{p.kind}", - f"Kind `{p.kind}`: " + " ".join(parts) - + f" Answer `locations.{p.kind}: {{: }}` to pick one.", - None, - ) - ) + questions.extend(location_questions) questions_open = [ q for q in questions if _answer(answers, q.key) is None ] @@ -1593,6 +1668,7 @@ def propose( "kinds": {}, } report: dict[str, Any] = { + "applied": applied, "dialect": dialect, "tables": len(schema.tables), "questions": [ @@ -1832,6 +1908,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: s.add_argument("--create-views", action="store_true", help="create a view for a table the SQL guard refuses by name (needs --admin-read)") s.add_argument("--admin-read", type=Path, help="admin.yaml: a connection allowed to CREATE VIEW and GRANT") s.add_argument("--reader-account", default="", help="the read account to GRANT SELECT on created views (MySQL: 'user'@'%%')") + s.add_argument("--grant-trigger", action="store_true", help="MySQL: GRANT TRIGGER to the read account through --admin-read, so the frame can see triggers") s.add_argument("--no-validate", action="store_true", help="skip the dry read (tests only)") args = ap.parse_args(argv) try: @@ -1897,6 +1974,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: create_views_flag=args.create_views, reader_account=args.reader_account, validate=not args.no_validate, + grant_trigger_flag=args.grant_trigger, ) print(f"questions: {len(report['questions'])} ({len(report['questions_open'])} open) -> {args.out / 'QUESTIONS.md'}") for kind, rep in report["kinds"].items(): From db75fd8d8a7e57f389a6974bc9c8f0ff12daf6ac Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:33:29 -0400 Subject: [PATCH 03/94] WIP: by-name update fields; keep answered questions counted --- .../generate/verify_contract.py | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index caccdbf34a..94d9aabfad 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -224,6 +224,8 @@ class KindPlan: unresolved: dict[str, list[Location]] = field(default_factory=dict) refused: list[str] = field(default_factory=list) removed: dict[str, int] = field(default_factory=dict) + #: Parameters placed by name, not by a demonstration. + by_name: list[str] = field(default_factory=list) def _scalars(values: Mapping[str, Any]) -> dict[str, Any]: @@ -341,6 +343,12 @@ def analyze( loc, tied = best(_intersect(found[p])) if loc is None: if not found[p]: + named = _by_name(schema, plan, p) + if named is not None: + plan.locations[p] = named + plan.by_name.append(p) + if plan.mode == "update": + plan.changes.append(p) continue # never given: no contract uses it plan.unresolved[p] = tied continue @@ -378,6 +386,27 @@ def analyze( return plan +def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: + """An update parameter no demonstration gives (``new_zip``), placed at + the main table's column of the same name after a leading ``new_`` or + ``set_`` (``zip``), when one exists. Compared exactly by the column's + type. Reported as placed by name; a demonstration that gives it replaces + this.""" + + if plan.mode != "update" or not plan.anchor: + return None + name = re.sub(r"^(new|set)_", "", param) + if name == param: + return None + col = schema.tables[plan.anchor].column(name) + if col is None or name in schema.tables[plan.anchor].primary_key: + return None + rule = {"number": "number", "bool": "number", "date": "date", "datetime": "datetime"}.get(col.kind, "exact") + if col.kind not in ("text", "number", "bool", "date", "datetime"): + return None + return Location((), (name,), rule) + + def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: """Group child-row locations, and find each list parameter's rows.""" @@ -1535,10 +1564,11 @@ def propose( p.changes.append(name) else: open_[name] = tied + asked = dict(p.unresolved) p.unresolved = open_ - if open_: + if asked: parts = [] - for name, tied in open_.items(): + for name, tied in asked.items(): if tied: opts = "; ".join(f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied)) parts.append(f"`{name}` is in several places: {opts}") @@ -1555,6 +1585,8 @@ def propose( None, ) ) + if not open_: + answers = {**answers, f"locations.{p.kind}": chosen} # Q: bookkeeping, for the whole app at once. logs = set().union(*(p.logs for p in plans)) if plans else set() book: dict[str, set[str]] = {} @@ -1691,6 +1723,7 @@ def propose( "mode": p.mode, "main_table": p.anchor, "locations": {k: v.describe(p.anchor) for k, v in sorted(p.locations.items())}, + "placed_by_name": sorted(p.by_name), "children": {k: g.table for k, g in p.children.items()}, "lists": {k: g.table for k, g in p.lists.items()}, "side_rows": [{"table": s.table, "count": s.count, "unless": s.unless} for s in p.sides], From 90af2ff8de0e7fd88c1bd490bcb9defe9d8a7237 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:34:36 -0400 Subject: [PATCH 04/94] WIP: copied-column name preference --- packages/flow/openadapt_flow/generate/demo_analysis.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 404033242f..e51a5e8a02 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -619,6 +619,15 @@ def _copied( candidates = here if candidates is None else candidates & here if not candidates: return None + if candidates and len(candidates) > 1: + # Several columns of the row it points to hold the same value: keep + # the one named like this column (``tva_tx`` from ``tva_tx``, then + # ``subprice`` from ``price``), if exactly one is. + for test in (lambda c: c[3] == name, lambda c: c[3] in name): + named = {c for c in candidates if test(c)} + if len(named) == 1: + candidates = named + break if candidates and len(candidates) == 1: via, target, ref, source = next(iter(candidates)) return {"via": via, "table": target, "ref_column": ref, "column": source} From 15e6c201706ca6eb71516334b641c34c1e375683 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:35:48 -0400 Subject: [PATCH 05/94] WIP: copied column prefers the table its key names --- packages/flow/openadapt_flow/generate/demo_analysis.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index e51a5e8a02..16f0a543b9 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -623,7 +623,14 @@ def _copied( # Several columns of the row it points to hold the same value: keep # the one named like this column (``tva_tx`` from ``tva_tx``, then # ``subprice`` from ``price``), if exactly one is. - for test in (lambda c: c[3] == name, lambda c: c[3] in name): + def stem(col: str) -> str: + return re.sub(r"(^fk_|_id$|_fk$|^id_)", "", col.lower()) + + for test in ( + lambda c: c[3] == name, + lambda c: c[3] in name, + lambda c: c[3] in name and c[1].lower().endswith(stem(c[0])), + ): named = {c for c in candidates if test(c)} if len(named) == 1: candidates = named From cc82064c2ccf47724e5ac000a4bc3b1a3fa37500 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:42:30 -0400 Subject: [PATCH 06/94] WIP: compare number and date columns by value; docs --- .../generate_contracts/demo_redmine.py | 122 ++++++++++ packages/flow/docs/GENERATE_CONTRACTS.md | 216 ++++++++++++++++++ .../openadapt_flow/generate/demo_analysis.py | 5 +- .../generate/ensure_contract.py | 7 +- .../generate/verify_contract.py | 2 +- 5 files changed, 349 insertions(+), 3 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/demo_redmine.py create mode 100644 packages/flow/docs/GENERATE_CONTRACTS.md diff --git a/packages/flow/benchmark/generate_contracts/demo_redmine.py b/packages/flow/benchmark/generate_contracts/demo_redmine.py new file mode 100644 index 0000000000..1135204fec --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/demo_redmine.py @@ -0,0 +1,122 @@ +"""Redmine (first study, instance 1): demonstrations, then the generator. + + FCG_INSTANCE=1 python benchmark/generate_contracts/demo_redmine.py WORKDIR + +One demonstration write per example in ``setup_view.yaml``, through +Redmine's REST API as the bot. Ids for names (project, tracker, user) are +looked up through the read-only account, as a setup agent would. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import psycopg2 +import requests + +from demo_lib import demo, fixture, propose, setup_view, write_inputs + +APP = "redmine" + + +def main(work: Path) -> None: + fx = fixture(APP) + fx.reset() + bot = fx.bot() + ro = fx.read_access() + read = { + "sql_driver": "psycopg2", + "sql_connect_args": {"host": ro["host"], "port": ro["port"], "user": ro["user"], "dbname": ro["database"]}, + "sql_password_env": "REDMINE_RO_DB_PASSWORD", + } + write_inputs(work, APP, read) + db = psycopg2.connect(ro["dsn"]) + db.autocommit = True + + def one(sql: str, args: tuple = ()) -> object: + with db.cursor() as cur: + cur.execute(sql, args) + rows = cur.fetchall() + if len(rows) != 1: + raise RuntimeError(f"{sql} {args}: {len(rows)} rows") + return rows[0][0] + + def project(name: str) -> int: + return int(one("SELECT id FROM projects WHERE name=%s", (name,))) + + def user(name: str) -> int: + return int(one("SELECT id FROM users WHERE firstname || ' ' || lastname = %s", (name,))) + + def enum(kind: str, name: str) -> int: + return int(one("SELECT id FROM enumerations WHERE type=%s AND name=%s", (kind, name))) + + def issue(proj: str, subject: str) -> int: + return int(one("SELECT i.id FROM issues i JOIN projects p ON p.id=i.project_id " + "WHERE p.name=%s AND i.subject=%s", (proj, subject))) # fmt: skip + + def field(name: str) -> int: + return int(one("SELECT id FROM custom_fields WHERE name=%s", (name,))) + + def api(method: str, path: str, payload: object) -> None: + r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r.raise_for_status() + + def create_payload(v: dict) -> dict: + body: dict = { + "project_id": project(v["project"]), + "tracker_id": int(one("SELECT id FROM trackers WHERE name=%s", (v["tracker"],))), + "subject": v["subject"], + "priority_id": enum("IssuePriority", v["priority"]), + } + if v.get("assignee"): + body["assigned_to_id"] = user(v["assignee"]) + if v.get("due_date"): + body["due_date"] = v["due_date"] + if v.get("description"): + body["description"] = v["description"] + cf = [ + {"id": field(n), "value": v[k]} + for k, n in (("customer_ref", "Customer Ref"), ("environment", "Environment")) + if v.get(k) is not None + ] + if cf: + body["custom_fields"] = cf + return {"issue": body} + + for k in setup_view(APP)["kinds"]: + kind = k["kind"] + for ex in k["examples"]: + v = ex["values"] + if kind in ("create_issue", "ensure_issue"): + if kind == "ensure_issue" and int(one( + "SELECT COUNT(*) FROM issues i JOIN projects p ON p.id=i.project_id " + "WHERE p.name=%s AND i.subject=%s", (v["project"], v["subject"]))): # fmt: skip + continue + demo(work, kind, v, lambda p=create_payload(v): api("POST", "/issues.json", p)) + elif kind == "update_issue": + body: dict = {} + if v.get("status"): + body["status_id"] = int(one("SELECT id FROM issue_statuses WHERE name=%s", (v["status"],))) + if v.get("assignee"): + body["assigned_to_id"] = user(v["assignee"]) + if v.get("priority"): + body["priority_id"] = enum("IssuePriority", v["priority"]) + for key in ("due_date", "done_ratio", "notes"): + if v.get(key) is not None: + body[key] = v[key] + iid = issue(v["project"], v["subject"]) + demo(work, kind, v, lambda i=iid, b=body: api("PUT", f"/issues/{i}.json", {"issue": b})) + elif kind == "log_time": + body = {"issue_id": issue(v["project"], v["issue_subject"]), "hours": v["hours"], + "spent_on": v["spent_on"], "activity_id": enum("TimeEntryActivity", v["activity"]), + "comments": v["comments"]} # fmt: skip + demo(work, kind, v, lambda b=body: api("POST", "/time_entries.json", {"time_entry": b})) + bot_id = one("SELECT id FROM users WHERE login=%s", (bot["login"],)) + report = propose(work, {"bot_user_id": str(bot_id)}) + print("questions:", len(report["questions"])) + fx.reset() + + +if __name__ == "__main__": + main(Path(sys.argv[1])) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md new file mode 100644 index 0000000000..53aab255ce --- /dev/null +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -0,0 +1,216 @@ +# Generate verify contracts from a SQL schema + +`python -m openadapt_flow.generate.verify_contract` writes the +`openadapt.verify/v1` contracts for an app's kinds of write. You give it a +read-only database account, a short description of each kind of write, and +one demonstration write per example. It asks a person only what it can't +decide alone, in a numbered list. The goal is three questions or fewer per +app. + +Use it for any app whose data is in MySQL, MariaDB or PostgreSQL. For a FHIR +server, use `openadapt_flow.generate.ensure_contract` (an +`openadapt.ensure-record/v1` contract). This guide needs no code reading. + +## What you need + +- **A read-only database account.** The contracts read with it, and so does + the generator. Put its connection in `read.yaml`, with the password in an + environment variable: + + ```yaml + sql_driver: pymysql # or psycopg2 for PostgreSQL + sql_connect_args: {host: 127.0.0.1, port: 3306, user: oa_reader, database: app} + sql_password_env: APP_READER_PASSWORD + ``` + + The generator refuses an account that can insert, update, delete or change + the schema. On MySQL and MariaDB the account also needs the `TRIGGER` + privilege, because the whole-database check must see triggers. The + generator can grant it (see [Setup steps it applies](#setup-steps-it-applies)). +- **`kinds.yaml`**: each kind of write, its parameter names, and one or two + example tasks. A null value means the task leaves that field out. A list + value (invoice lines) is a list of mappings. + + ```yaml + kinds: + - kind: update_customer + parameters: [customer, town, new_phone, new_email, new_address, new_zip] + examples: + - values: {customer: Transports Petit, town: Rouen, new_phone: 02 35 15 60 60} + - values: {customer: Pharmacie Moreau, town: Lille, new_phone: 03 20 06 07 08, + new_email: officine@pharmacie-moreau-lille.fr} + ``` + +- **The acting user's id**, as the caller passes it to the verifier, for + example `--actor-param bot_user_id=5`. A column that holds it (an author or + creator column) is checked against it. + +## Steps + +1. Check the account and read the schema: + + ```bash + python -m openadapt_flow.generate.verify_contract schema --read read.yaml + ``` + +2. For each example, make the write once, correctly, the way the agent will + (through the app's API or screens), between a snapshot and a record: + + ```bash + python -m openadapt_flow.generate.verify_contract snapshot --read read.yaml --out work/before.json + # ... make the write ... + python -m openadapt_flow.generate.verify_contract record --read read.yaml \ + --before work/before.json --kind update_customer \ + --values '{"customer": "Transports Petit", "town": "Rouen", "new_phone": "02 35 15 60 60"}' \ + --demos work/demos + ``` + + Skip an example whose record already exists ("make sure X exists"): the + right outcome there is no write. Snapshots hold row values. Keep the work + folder private, like a database export. + +3. Propose: + + ```bash + python -m openadapt_flow.generate.verify_contract propose --read read.yaml \ + --kinds kinds.yaml --demos work/demos --out work/out --actor-param bot_user_id=5 + ``` + +4. Read `work/out/QUESTIONS.md`. Write the answers in `answers.yaml` and run + `propose` again with `--answers answers.yaml`. Until every question is + answered, the manifest says so and `choose` refuses to pick a contract. + +## What it writes + +- `contracts//..yaml`: one contract per field set. The + flavor is `create` and `found` for a kind that adds a record, `update` for a + kind that changes one. +- `manifest.yaml`: which contract fits which task values. +- `QUESTIONS.md`: the numbered questions, the proposed answers, and per kind + how many contracts were written and why any were not. +- `report.json`: where each parameter was found, the rows the app adds on its + own, the tables treated as logs, and every contract that was not written, + with the reason. + +At run time, pick the contract for a task with `choose`, then call the +verifier as usual: + +```python +from openadapt_flow.generate.verify_contract import choose +from openadapt_flow.verifier import Actor, begin_write, verify_write + +contract, params = choose("work/out/manifest.yaml", "update_customer", task_values, + {"bot_user_id": "5"}) +pending = begin_write(contract, params, actor=Actor("agent", "bot")) +... # the agent writes +result = verify_write(pending) +``` + +`choose` raises `GenerateError` when no contract fits the task's values. Treat +that as a stop: the write must not be sent unchecked. + +## How it decides + +**The main record.** In each demonstration, the main record is the added row +(a create) or changed row (an update) that holds the most task values. + +**Where a value is.** Each value is looked up by exact rules only: + +| Rule | Example | +| --- | --- | +| The stored text equals the value | `subject` | +| Equal after case folding (an email address) | `Ana@Example.org` | +| Equal digits (a phone number) | `02 38 53 21 09` stored as `0238532109` | +| Equal as a number, a date or a date-time, compared in SQL | `9.20` stored as `9.2`; `2026-10-27 14:30` stored as `2026-10-27 14:30:00` | +| Minutes between two date-time columns of the row | a call's duration | +| A display column of the row a key column points to | `project_id` -> `projects.name` | +| Two text columns joined by one space | `firstname` and `lastname` | +| A row the write added that holds the main record's key | a journal note, a custom field value | + +A key column can be a declared foreign key, or a column whose value is +another table's primary key. When several places fit in every demonstration, +the generator keeps the shortest path, then a declared key, then a column +named after its table, then the most exact rule. If places are still tied, +that is a question. + +An update parameter that no example gives, such as `new_zip`, is placed at the +main table's column with the same name after a leading `new_` or `set_`, when +there is one. The report lists such parameters under `placed_by_name`. + +**Field sets.** A task's field set is its non-null values. For an update, the +generator writes a contract for every non-empty set of the fields to change +that it found, up to 64. For a create, it writes one per demonstrated set, +plus the sets with an optional field (null in some example) left out. A +field left out of a create must be empty in the new record. A list parameter +gets a contract per length, from 1 to 10 items. + +**Create, found, update.** + +- `create`: exactly one new main record holds the task's values, and + everything else in the new row is what the demonstrations showed, or is + bookkeeping. +- `found`: the record already exists, and nothing in the database changed. + `choose` reads the store first and picks `found` when a record with the + task's values is already there. +- `update`: exactly one record matches the task's identity values and holds + the new values. Only the fields the task changes may change. + +**Rows the app adds on its own.** A row type the app added the same number of +times in every demonstration (an agenda event, a stream note, a price-history +row) is declared with that count and its constant columns. A table whose +added rows vary between demonstrations, or whose name says log, history, +audit or similar, is proposed as a log. Columns that changed with no task +value (a modified time, a version counter) are proposed as bookkeeping. +Both go in one question. + +**The frame.** On MySQL and MariaDB every contract uses the whole-database +frame (`frame.database`): a row added, removed or changed in any table that +the contract doesn't declare stops the write. The main record's table and +the tables of rows that hold values are read in full by the contract's own +reads, with every column. On PostgreSQL, `frame.database` isn't available, so +every other table is read as its own surface; tables proposed as logs are +not read there. + +**The recheck window.** On MySQL and MariaDB, one question asks how long the +app can still write on its own after a verified write. The answer is +`frame.database.recheck_after_s`; the proposal is 300 seconds. + +## Setup steps it applies + +The generator never leaves a step for a person to apply later. A contract +that needs something the store doesn't have is either set up, with a flag +and an admin connection you give, or not written, with the reason. + +- `--grant-trigger --admin-read admin.yaml --reader-account "'oa_reader'@'%'"`: + grants `TRIGGER` on the database to the read account (MySQL, MariaDB). + Without it, the generator stops and prints the `GRANT` statement for the + DBA. Note that `TRIGGER` also lets the account create triggers. The verifier + needs it only to see them. +- `--create-views --admin-read admin.yaml --reader-account ...`: the + verifier's SQL guard refuses some table names in a read (EspoCRM's `call`). + This creates a view `oa_read_` and grants the read account `SELECT` + on it. Without the flag, the kind is refused with the reason. + +Every contract is loaded and read once against the live store before it is +written out. A contract whose read fails is not written; `report.json` says +why. + +## Limits + +- A parameter that no example gives is placed only by the name rule above. + Otherwise a task that gives it has no contract and is stopped. Add an + example and a demonstration. +- A value the demonstrations hold in no row, or in a place none of the rules + describe, is a question. +- A list item that appears twice in one task (the same product and quantity + twice) is stopped: each item must match one new row. +- Columns of a new record that differ between demonstrations and aren't + task values (an invoice's computed totals, its draft reference) are not + checked. The bookkeeping question lists them. +- On PostgreSQL there is no whole-database frame and no recheck: tables + proposed as logs are not read, and a write that lands after the check is + not seen. +- REST metadata (OpenAPI) is not read yet. Apps without database access need + hand-written contracts. +- The generator does not read an audit trail, so another user's edit during + the window stops the write instead of being attributed to them. diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 16f0a543b9..b76d765c5e 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -99,7 +99,10 @@ def rule_for(stored: Optional[str], column_kind: str, value: Any) -> Optional[st text = value_text(value) if text == "": return None - if stored == text: + if stored == text and column_kind not in ("number", "bool", "date", "datetime"): + # A number or date column is compared by value even when this + # demonstration's text happens to match: another row of the same + # column may print as ``5.5000``. return "exact" if column_kind == "text": if "@" in text and stored.casefold() == text.casefold(): diff --git a/packages/flow/openadapt_flow/generate/ensure_contract.py b/packages/flow/openadapt_flow/generate/ensure_contract.py index 4660fd1455..a34a3747bb 100644 --- a/packages/flow/openadapt_flow/generate/ensure_contract.py +++ b/packages/flow/openadapt_flow/generate/ensure_contract.py @@ -824,7 +824,12 @@ def render_yaml(gen: Generated, *, generator: str = "") -> str: def main(argv: Optional[Sequence[str]] = None) -> int: - ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap = argparse.ArgumentParser( + description=__doc__.splitlines()[0] + + " For an app whose data is in MySQL, MariaDB or PostgreSQL, use " + "`python -m openadapt_flow.generate.verify_contract` instead " + "(docs/GENERATE_CONTRACTS.md)." + ) ap.add_argument("teach", help="teach directory") ap.add_argument("--out", help="write the contract YAML here when admitted") ap.add_argument("--report", help="write the JSON report here") diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 94d9aabfad..a69f8d0a82 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1130,7 +1130,7 @@ def _raw_effect( c = t.column(col) lit = value.get("literal") if isinstance(value, dict) else None if c is not None and lit is not None and not ( - c.kind == "text" or (c.kind in ("number", "bool") and re.fullmatch(r"-?\d+", lit)) + c.kind == "text" or (c.kind in ("number", "bool") and c.data_type.startswith(("int", "tinyint", "smallint", "mediumint", "bigint", "integer")) and re.fullmatch(r"-?\d+", lit)) ): m.pop(col) nrf = set(getattr(surf, "_new_row_fields", set())) From 1173b66b38dec9aa2f9ae1d7210e8c35336806ad Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:48:18 -0400 Subject: [PATCH 07/94] WIP: free text unchecked on new records; co-owned child rows; demos for EspoCRM and Redmine --- .../generate_contracts/demo_dolibarr.py | 9 +- .../generate_contracts/demo_espocrm.py | 125 ++++++++++++++++++ .../openadapt_flow/generate/demo_analysis.py | 6 + .../generate/verify_contract.py | 41 +++++- 4 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/demo_espocrm.py diff --git a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py index 4fd6a683ac..45023b6949 100644 --- a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py +++ b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py @@ -63,14 +63,19 @@ def product(ref: str) -> tuple[int, str, str]: kind = k["kind"] if kind == "create_customer": if customer(v["name"], v["town"]) is not None: - continue + # Already there (a "make sure" example): demonstrate a + # second create with values of our own instead. + v = {**v, "name": "Atelier Démo Générateur", "address": "8 place du Marché", + "zip": "21000", "town": "Dijon", "phone": "03 80 30 31 32", + "email": "atelier@demo-generateur.example"} payload = {"name": v["name"], "address": v["address"], "zip": v["zip"], "town": v["town"], "country_id": country(v["country"]), "phone": v["phone"], "email": v["email"], "client": 1} # fmt: skip demo(work, kind, v, lambda p=payload: api("POST", "/thirdparties", p)) elif kind == "create_product": db.commit() if q("SELECT 1 FROM llx_product WHERE BINARY ref=%s", (v["ref"],)): - continue + v = {**v, "ref": "GEN-DEMO-2", "label": "Coffret démo générateur", + "price_excl_vat": "12.40", "vat_rate": "20"} payload = {"ref": v["ref"], "label": v["label"], "type": 0, "price": v["price_excl_vat"], "price_base_type": "HT", "tva_tx": v["vat_rate"], "status": 1, "status_buy": 1} # fmt: skip demo(work, kind, v, lambda p=payload: api("POST", "/products", p)) diff --git a/packages/flow/benchmark/generate_contracts/demo_espocrm.py b/packages/flow/benchmark/generate_contracts/demo_espocrm.py new file mode 100644 index 0000000000..6d13b7ccaa --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/demo_espocrm.py @@ -0,0 +1,125 @@ +"""EspoCRM (first study, instance 2): demonstrations, then the generator. + + FCG_INSTANCE=2 python benchmark/generate_contracts/demo_espocrm.py WORKDIR + +One demonstration write per example in ``setup_view.yaml``, through +EspoCRM's REST API as the API user ``crm-bot``. Record ids for names are +looked up through the read-only account (exact, byte for byte: the name +columns use a case- and accent-insensitive collation). +""" + +from __future__ import annotations + +import datetime as dt +import sys +from pathlib import Path + +import pymysql +import requests + +from demo_lib import demo, fixture, propose, setup_view, write_inputs + +APP = "espocrm" + + +def main(work: Path, extra: list[str]) -> None: + fx = fixture(APP) + fx.reset() + bot = fx.bot() + ro = fx.read_access() + read = { + "sql_driver": "pymysql", + "sql_connect_args": {"host": ro["host"], "port": ro["port"], "user": ro["user"], + "database": ro["database"], "charset": "utf8mb4"}, + "sql_password_env": "ESPOCRM_DB_RO_PASSWORD", + } # fmt: skip + write_inputs(work, APP, read) + db = pymysql.connect(host=ro["host"], port=ro["port"], user=ro["user"], password=ro["password"], + database=ro["database"], charset="utf8mb4", autocommit=True) # fmt: skip + + def one(sql: str, args: tuple = ()) -> str: + with db.cursor() as cur: + cur.execute(sql, args) + rows = cur.fetchall() + if len(rows) != 1: + raise RuntimeError(f"{sql} {args}: {len(rows)} rows") + return str(rows[0][0]) + + def account(name: str, city: str) -> str: + return one("SELECT id FROM account WHERE BINARY name=%s AND BINARY billing_address_city=%s AND deleted=0", (name, city)) + + def person(table: str, full: str, company: str | None = None) -> str: + first, last = full.split(" ", 1) + sql = f"SELECT id FROM {table} WHERE BINARY first_name=%s AND BINARY last_name=%s AND deleted=0" + args: tuple = (first, last) + if company is not None: + sql += " AND BINARY account_name=%s" + args += (company,) + return one(sql, args) + + def api(method: str, path: str, payload: dict) -> None: + r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r.raise_for_status() + + bot_id = one("SELECT id FROM user WHERE user_name=%s", ("crm-bot",)) + for k in setup_view(APP)["kinds"]: + kind = k["kind"] + for ex in k["examples"]: + v = ex["values"] + if kind == "create_contact": + with db.cursor() as cur: + cur.execute("SELECT COUNT(*) FROM contact WHERE BINARY first_name=%s AND BINARY last_name=%s AND deleted=0", + (v["first_name"], v["last_name"])) # fmt: skip + if cur.fetchone()[0]: + continue + body = {"firstName": v["first_name"], "lastName": v["last_name"], + "accountId": account(v["account"], v["account_city"])} # fmt: skip + if v.get("title"): + body["title"] = v["title"] + if v.get("email"): + body["emailAddress"] = v["email"] + if v.get("phone"): + body["phoneNumber"] = v["phone"] + demo(work, kind, v, lambda b=body: api("POST", "/Contact", b)) + elif kind == "create_opportunity": + with db.cursor() as cur: + cur.execute("SELECT COUNT(*) FROM opportunity WHERE BINARY name=%s AND deleted=0", (v["name"],)) + if cur.fetchone()[0]: + continue + body = {"name": v["name"], "accountId": account(v["account"], v["account_city"]), + "amount": v["amount"], "amountCurrency": v["currency"], "stage": v["stage"], + "closeDate": v["close_date"]} # fmt: skip + demo(work, kind, v, lambda b=body: api("POST", "/Opportunity", b)) + elif kind == "update_lead": + lid = one("SELECT id FROM lead WHERE BINARY first_name=%s AND BINARY last_name=%s " + "AND BINARY account_name=%s AND deleted=0", + (v["first_name"], v["last_name"], v["company"])) # fmt: skip + if v["field"] == "email": + body = {"emailAddressData": [{"emailAddress": v["value"], "primary": True, + "optOut": False, "invalid": False}]} # fmt: skip + else: + body = {v["field"]: v["value"]} + demo(work, kind, v, lambda i=lid, b=body: api("PUT", f"/Lead/{i}", b)) + elif kind == "log_call": + start = dt.datetime.strptime(v["date_start"], "%Y-%m-%d %H:%M") + end = start + dt.timedelta(minutes=int(v["duration_minutes"])) + body = {"name": v["subject"], "status": v["status"], "direction": v["direction"], + "dateStart": start.strftime("%Y-%m-%d %H:%M:%S"), + "dateEnd": end.strftime("%Y-%m-%d %H:%M:%S"), "assignedUserId": bot_id} # fmt: skip + if v.get("account"): + body.update(parentType="Account", parentId=account(v["account"], v["account_city"])) + if v.get("lead"): + lead = person("lead", v["lead"], v.get("lead_company")) + body.update(parentType="Lead", parentId=lead, leadsIds=[lead]) + if v.get("contact"): + body["contactsIds"] = [person("contact", v["contact"])] + if v.get("description"): + body["description"] = v["description"] + demo(work, kind, v, lambda b=body: api("POST", "/Call", b)) + report = propose(work, {"bot_user_id": bot_id}, extra) + print("questions:", len(report["questions"])) + fx.reset() + + +if __name__ == "__main__": + main(Path(sys.argv[1]), sys.argv[2:]) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index b76d765c5e..a166aa5141 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -572,6 +572,12 @@ def column_roles( out[name] = ColumnRole("copied", detail=copied) continue distinct = {v for v in values} + if col.kind == "text" and distinct <= {None, ""}: + # Free text the task doesn't give and the demonstrations left + # empty (a line's description, a note): an agent may fill it + # from the record it copies, so it isn't checked. + out[name] = ColumnRole("free") + continue if len(distinct) == 1: v = values[0] default = col.default diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index a69f8d0a82..1943708792 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -185,6 +185,10 @@ class ChildGroup: #: List parameter whose items these rows are, if any. list_param: Optional[str] = None changed_columns: list[str] = field(default_factory=list) + #: Other parameters this row also points to (a contact's account link + #: row also holds the title): with one of them given and this one not, + #: the row is still expected, with this value empty. + co_owners: list[str] = field(default_factory=list) @dataclass @@ -429,12 +433,25 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: break explained = {loc.hops[0].column, *rel.columns} explained |= {h.column for h in rel.hops[:1]} + co: list[str] = [] + for q in plan.params: + if q == p or not rows: + continue + hits = [ + locate(schema, d, _as_change(loc.hops[0].table, r), d.values[q], max_hops=1) + for d, r in rows + if d.values.get(q) is not None and not isinstance(d.values.get(q), (list, dict)) + ] + if hits and all(h and h[0].hops for h in hits): + co.append(q) + explained |= {h[0].hops[0].column for h in hits} plan.children[p] = ChildGroup( loc.hops[0].table, loc.hops[0], {p: rel}, column_roles(schema, loc.hops[0].table, rows, explained, {}), 1, + co_owners=co, ) for p in lists: per_key: dict[str, list[list[Location]]] = {} @@ -906,7 +923,11 @@ def build_contract( # Rows beside the main record that hold a value (a note, a custom field, # an email address link), and list items (invoice lines). if flavor != "found": - groups = [(p, g) for p, g in plan.children.items() if p in fields] + groups = [ + (p, g) + for p, g in plan.children.items() + if p in fields or (plan.mode == "create" and set(g.co_owners) & fields) + ] groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] for p, g in groups: name = f"rows:{g.table}" @@ -940,6 +961,14 @@ def build_contract( m[f"main__{_alias(ip)}"] = _value_expr(ip, loc.rule) for k, loc in g.params.items(): pname = k if g.list_param is None else f"{p}.{i}.{k}" + if g.list_param is None and pname not in fields: + # Given another owner but not this value: empty. + for c in loc.columns if not loc.hops else [loc.hops[0].column]: + alias = f"empty__{_alias(c)}" + if alias not in {a for a, _ in surf.columns}: + surf.columns.append((alias, b.empty("t", g.table, c))) + m[alias] = {"literal": "1"} + continue key = _place(b, surf, g.table, pname, loc) if loc.rule in ("number", "date", "datetime", "minutes"): m[key] = {"literal": "1"} @@ -1141,7 +1170,7 @@ def _raw_effect( if role is not None and role.role == "constant" and role.value is None: continue if role is None or role.role != "constant" or (role.detail or {}).get("is_default"): - if role is None or role.role in ("key", "timestamp", "link", "varies", "copied", "actor"): + if role is None or role.role in ("key", "timestamp", "link", "varies", "copied", "actor", "free"): nrf.add(c.name) continue nrf.add(c.name) @@ -1724,6 +1753,7 @@ def propose( "main_table": p.anchor, "locations": {k: v.describe(p.anchor) for k, v in sorted(p.locations.items())}, "placed_by_name": sorted(p.by_name), + "demonstrations": len(p.demos), "children": {k: g.table for k, g in p.children.items()}, "lists": {k: g.table for k, g in p.lists.items()}, "side_rows": [{"table": s.table, "count": s.count, "unless": s.unless} for s in p.sides], @@ -1805,6 +1835,13 @@ def _questions_md(questions: Sequence[Question], answers: Mapping[str, Any], rep lines.append("") for kind, rep in report["kinds"].items(): lines.append(f"- `{kind}` ({rep['mode'] or 'no contract'}, main table `{rep['main_table']}`): {rep['emitted']} contract(s).") + if rep.get("demonstrations") == 1 and rep["mode"] == "create": + lines.append( + " - One demonstration only: every column of a new record that the task " + "doesn't give must hold the value it held then. Record a second create " + "with other values, so a column computed from the values (a price with " + "tax) is told apart from a constant." + ) for r in rep["refused"]: lines.append(f" - Refused: {r}") for r in rep["rejected"][:5]: From 2865491a1a98e5a5aa3b76ff8d55b74571454bcc Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:03:22 -0400 Subject: [PATCH 08/94] WIP: format; summarize script; connection hygiene in demos --- .../generate_contracts/demo_dolibarr.py | 79 ++- .../generate_contracts/demo_espocrm.py | 25 +- .../benchmark/generate_contracts/demo_lib.py | 12 +- .../generate_contracts/demo_redmine.py | 62 ++- .../benchmark/generate_contracts/summarize.py | 81 +++ .../benchmark/generate_contracts/validate.py | 39 +- .../openadapt_flow/generate/demo_analysis.py | 58 +- .../flow/openadapt_flow/generate/sql_store.py | 26 +- .../generate/verify_contract.py | 523 ++++++++++++++---- .../tests/test_generate_verify_contract.py | 141 ++++- 10 files changed, 840 insertions(+), 206 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/summarize.py diff --git a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py index 45023b6949..85bcc4a74a 100644 --- a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py +++ b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py @@ -15,7 +15,6 @@ import pymysql import requests - from demo_lib import demo, fixture, propose, setup_view, write_inputs APP = "dolibarr" @@ -28,11 +27,23 @@ def main(work: Path) -> None: ro = fx.read_access() read = { "sql_driver": "pymysql", - "sql_connect_args": {"host": ro["host"], "port": ro["port"], "user": ro["user"], "database": ro["database"]}, + "sql_connect_args": { + "host": ro["host"], + "port": ro["port"], + "user": ro["user"], + "database": ro["database"], + }, "sql_password_env": "FCG_DOLIBARR_READER_PASSWORD", } write_inputs(work, APP, read) - db = pymysql.connect(host=ro["host"], port=ro["port"], user=ro["user"], password=ro["password"], database=ro["database"]) + db = pymysql.connect( + host=ro["host"], + port=ro["port"], + user=ro["user"], + password=ro["password"], + database=ro["database"], + autocommit=True, + ) def q(sql: str, args: tuple = ()) -> list[tuple]: with db.cursor() as cur: @@ -40,13 +51,22 @@ def q(sql: str, args: tuple = ()) -> list[tuple]: return list(cur.fetchall()) def api(method: str, path: str, payload: object = None) -> object: - r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r = requests.request( + method, + bot["base_url"] + path, + headers=bot["headers"], + json=payload, + timeout=60, + ) r.raise_for_status() return r.json() def customer(name: str, town: str) -> int | None: db.commit() - rows = q("SELECT rowid FROM llx_societe WHERE BINARY nom=%s AND BINARY town=%s", (name, town)) + rows = q( + "SELECT rowid FROM llx_societe WHERE BINARY nom=%s AND BINARY town=%s", + (name, town), + ) return int(rows[0][0]) if len(rows) == 1 else None def country(label: str) -> int: @@ -54,7 +74,9 @@ def country(label: str) -> int: def product(ref: str) -> tuple[int, str, str]: db.commit() - rid, price, tva = q("SELECT rowid, price, tva_tx FROM llx_product WHERE BINARY ref=%s", (ref,))[0] + rid, price, tva = q( + "SELECT rowid, price, tva_tx FROM llx_product WHERE BINARY ref=%s", (ref,) + )[0] return int(rid), str(price), str(tva) for k in setup_view(APP)["kinds"]: @@ -65,17 +87,28 @@ def product(ref: str) -> tuple[int, str, str]: if customer(v["name"], v["town"]) is not None: # Already there (a "make sure" example): demonstrate a # second create with values of our own instead. - v = {**v, "name": "Atelier Démo Générateur", "address": "8 place du Marché", - "zip": "21000", "town": "Dijon", "phone": "03 80 30 31 32", - "email": "atelier@demo-generateur.example"} + v = { + **v, + "name": "Atelier Démo Générateur", + "address": "8 place du Marché", + "zip": "21000", + "town": "Dijon", + "phone": "03 80 30 31 32", + "email": "atelier@demo-generateur.example", + } payload = {"name": v["name"], "address": v["address"], "zip": v["zip"], "town": v["town"], "country_id": country(v["country"]), "phone": v["phone"], "email": v["email"], "client": 1} # fmt: skip demo(work, kind, v, lambda p=payload: api("POST", "/thirdparties", p)) elif kind == "create_product": db.commit() if q("SELECT 1 FROM llx_product WHERE BINARY ref=%s", (v["ref"],)): - v = {**v, "ref": "GEN-DEMO-2", "label": "Coffret démo générateur", - "price_excl_vat": "12.40", "vat_rate": "20"} + v = { + **v, + "ref": "GEN-DEMO-2", + "label": "Coffret démo générateur", + "price_excl_vat": "12.40", + "vat_rate": "20", + } payload = {"ref": v["ref"], "label": v["label"], "type": 0, "price": v["price_excl_vat"], "price_base_type": "HT", "tva_tx": v["vat_rate"], "status": 1, "status_buy": 1} # fmt: skip demo(work, kind, v, lambda p=payload: api("POST", "/products", p)) @@ -84,17 +117,35 @@ def product(ref: str) -> tuple[int, str, str]: lines = [] for ln in v["lines"]: pid, price, tva = product(ln["product_ref"]) - lines.append({"fk_product": pid, "qty": ln["qty"], "subprice": price, "tva_tx": tva, "product_type": 0}) + lines.append( + { + "fk_product": pid, + "qty": ln["qty"], + "subprice": price, + "tva_tx": tva, + "product_type": 0, + } + ) payload = {"socid": customer(v["customer"], v["town"]), "type": 0, "date": calendar.timegm((y, m, d, 12, 0, 0, 0, 0, 0)), "lines": lines} # fmt: skip demo(work, kind, v, lambda p=payload: api("POST", "/invoices", p)) elif kind == "update_customer": cid = customer(v["customer"], v["town"]) - change = {key[4:]: x for key, x in v.items() if key.startswith("new_") and x is not None} - demo(work, kind, v, lambda c=cid, p=change: api("PUT", f"/thirdparties/{c}", p)) + change = { + key[4:]: x + for key, x in v.items() + if key.startswith("new_") and x is not None + } + demo( + work, + kind, + v, + lambda c=cid, p=change: api("PUT", f"/thirdparties/{c}", p), + ) bot_id = q("SELECT rowid FROM llx_user WHERE login='fcg_bot'")[0][0] report = propose(work, {"bot_user_id": str(bot_id)}) print("questions:", len(report["questions"])) + db.close() # an open read transaction would block the reset fx.reset() diff --git a/packages/flow/benchmark/generate_contracts/demo_espocrm.py b/packages/flow/benchmark/generate_contracts/demo_espocrm.py index 6d13b7ccaa..3b6c3ca9da 100644 --- a/packages/flow/benchmark/generate_contracts/demo_espocrm.py +++ b/packages/flow/benchmark/generate_contracts/demo_espocrm.py @@ -16,7 +16,6 @@ import pymysql import requests - from demo_lib import demo, fixture, propose, setup_view, write_inputs APP = "espocrm" @@ -46,7 +45,10 @@ def one(sql: str, args: tuple = ()) -> str: return str(rows[0][0]) def account(name: str, city: str) -> str: - return one("SELECT id FROM account WHERE BINARY name=%s AND BINARY billing_address_city=%s AND deleted=0", (name, city)) + return one( + "SELECT id FROM account WHERE BINARY name=%s AND BINARY billing_address_city=%s AND deleted=0", + (name, city), + ) def person(table: str, full: str, company: str | None = None) -> str: first, last = full.split(" ", 1) @@ -58,7 +60,13 @@ def person(table: str, full: str, company: str | None = None) -> str: return one(sql, args) def api(method: str, path: str, payload: dict) -> None: - r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r = requests.request( + method, + bot["base_url"] + path, + headers=bot["headers"], + json=payload, + timeout=60, + ) r.raise_for_status() bot_id = one("SELECT id FROM user WHERE user_name=%s", ("crm-bot",)) @@ -83,7 +91,10 @@ def api(method: str, path: str, payload: dict) -> None: demo(work, kind, v, lambda b=body: api("POST", "/Contact", b)) elif kind == "create_opportunity": with db.cursor() as cur: - cur.execute("SELECT COUNT(*) FROM opportunity WHERE BINARY name=%s AND deleted=0", (v["name"],)) + cur.execute( + "SELECT COUNT(*) FROM opportunity WHERE BINARY name=%s AND deleted=0", + (v["name"],), + ) if cur.fetchone()[0]: continue body = {"name": v["name"], "accountId": account(v["account"], v["account_city"]), @@ -107,7 +118,10 @@ def api(method: str, path: str, payload: dict) -> None: "dateStart": start.strftime("%Y-%m-%d %H:%M:%S"), "dateEnd": end.strftime("%Y-%m-%d %H:%M:%S"), "assignedUserId": bot_id} # fmt: skip if v.get("account"): - body.update(parentType="Account", parentId=account(v["account"], v["account_city"])) + body.update( + parentType="Account", + parentId=account(v["account"], v["account_city"]), + ) if v.get("lead"): lead = person("lead", v["lead"], v.get("lead_company")) body.update(parentType="Lead", parentId=lead, leadsIds=[lead]) @@ -118,6 +132,7 @@ def api(method: str, path: str, payload: dict) -> None: demo(work, kind, v, lambda b=body: api("POST", "/Call", b)) report = propose(work, {"bot_user_id": bot_id}, extra) print("questions:", len(report["questions"])) + db.close() fx.reset() diff --git a/packages/flow/benchmark/generate_contracts/demo_lib.py b/packages/flow/benchmark/generate_contracts/demo_lib.py index f0f82bf224..40499cc211 100644 --- a/packages/flow/benchmark/generate_contracts/demo_lib.py +++ b/packages/flow/benchmark/generate_contracts/demo_lib.py @@ -48,7 +48,9 @@ def fixture(app: str) -> Any: def setup_view(app: str) -> dict[str, Any]: - return yaml.safe_load((FCG / "apps" / app / "setup_view.yaml").read_text(encoding="utf-8")) + return yaml.safe_load( + (FCG / "apps" / app / "setup_view.yaml").read_text(encoding="utf-8") + ) def write_inputs(work: Path, app: str, read: dict[str, Any]) -> list[str]: @@ -68,11 +70,15 @@ def write_inputs(work: Path, app: str, read: dict[str, Any]) -> list[str]: for k in view["kinds"] ] } - (work / "kinds.yaml").write_text(yaml.safe_dump(kinds, allow_unicode=True), encoding="utf-8") + (work / "kinds.yaml").write_text( + yaml.safe_dump(kinds, allow_unicode=True), encoding="utf-8" + ) return ["--read", str(work / "read.yaml")] -def demo(work: Path, kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: +def demo( + work: Path, kind: str, values: dict[str, Any], write: Callable[[], Any] +) -> None: """Snapshot, make one demonstration write, record the diff.""" flags = ["--read", str(work / "read.yaml")] diff --git a/packages/flow/benchmark/generate_contracts/demo_redmine.py b/packages/flow/benchmark/generate_contracts/demo_redmine.py index 1135204fec..6f643913ad 100644 --- a/packages/flow/benchmark/generate_contracts/demo_redmine.py +++ b/packages/flow/benchmark/generate_contracts/demo_redmine.py @@ -14,7 +14,6 @@ import psycopg2 import requests - from demo_lib import demo, fixture, propose, setup_view, write_inputs APP = "redmine" @@ -27,7 +26,12 @@ def main(work: Path) -> None: ro = fx.read_access() read = { "sql_driver": "psycopg2", - "sql_connect_args": {"host": ro["host"], "port": ro["port"], "user": ro["user"], "dbname": ro["database"]}, + "sql_connect_args": { + "host": ro["host"], + "port": ro["port"], + "user": ro["user"], + "dbname": ro["database"], + }, "sql_password_env": "REDMINE_RO_DB_PASSWORD", } write_inputs(work, APP, read) @@ -46,10 +50,14 @@ def project(name: str) -> int: return int(one("SELECT id FROM projects WHERE name=%s", (name,))) def user(name: str) -> int: - return int(one("SELECT id FROM users WHERE firstname || ' ' || lastname = %s", (name,))) + return int( + one("SELECT id FROM users WHERE firstname || ' ' || lastname = %s", (name,)) + ) def enum(kind: str, name: str) -> int: - return int(one("SELECT id FROM enumerations WHERE type=%s AND name=%s", (kind, name))) + return int( + one("SELECT id FROM enumerations WHERE type=%s AND name=%s", (kind, name)) + ) def issue(proj: str, subject: str) -> int: return int(one("SELECT i.id FROM issues i JOIN projects p ON p.id=i.project_id " @@ -59,13 +67,21 @@ def field(name: str) -> int: return int(one("SELECT id FROM custom_fields WHERE name=%s", (name,))) def api(method: str, path: str, payload: object) -> None: - r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r = requests.request( + method, + bot["base_url"] + path, + headers=bot["headers"], + json=payload, + timeout=60, + ) r.raise_for_status() def create_payload(v: dict) -> dict: body: dict = { "project_id": project(v["project"]), - "tracker_id": int(one("SELECT id FROM trackers WHERE name=%s", (v["tracker"],))), + "tracker_id": int( + one("SELECT id FROM trackers WHERE name=%s", (v["tracker"],)) + ), "subject": v["subject"], "priority_id": enum("IssuePriority", v["priority"]), } @@ -77,7 +93,10 @@ def create_payload(v: dict) -> dict: body["description"] = v["description"] cf = [ {"id": field(n), "value": v[k]} - for k, n in (("customer_ref", "Customer Ref"), ("environment", "Environment")) + for k, n in ( + ("customer_ref", "Customer Ref"), + ("environment", "Environment"), + ) if v.get(k) is not None ] if cf: @@ -93,11 +112,21 @@ def create_payload(v: dict) -> dict: "SELECT COUNT(*) FROM issues i JOIN projects p ON p.id=i.project_id " "WHERE p.name=%s AND i.subject=%s", (v["project"], v["subject"]))): # fmt: skip continue - demo(work, kind, v, lambda p=create_payload(v): api("POST", "/issues.json", p)) + demo( + work, + kind, + v, + lambda p=create_payload(v): api("POST", "/issues.json", p), + ) elif kind == "update_issue": body: dict = {} if v.get("status"): - body["status_id"] = int(one("SELECT id FROM issue_statuses WHERE name=%s", (v["status"],))) + body["status_id"] = int( + one( + "SELECT id FROM issue_statuses WHERE name=%s", + (v["status"],), + ) + ) if v.get("assignee"): body["assigned_to_id"] = user(v["assignee"]) if v.get("priority"): @@ -106,15 +135,26 @@ def create_payload(v: dict) -> dict: if v.get(key) is not None: body[key] = v[key] iid = issue(v["project"], v["subject"]) - demo(work, kind, v, lambda i=iid, b=body: api("PUT", f"/issues/{i}.json", {"issue": b})) + demo( + work, + kind, + v, + lambda i=iid, b=body: api("PUT", f"/issues/{i}.json", {"issue": b}), + ) elif kind == "log_time": body = {"issue_id": issue(v["project"], v["issue_subject"]), "hours": v["hours"], "spent_on": v["spent_on"], "activity_id": enum("TimeEntryActivity", v["activity"]), "comments": v["comments"]} # fmt: skip - demo(work, kind, v, lambda b=body: api("POST", "/time_entries.json", {"time_entry": b})) + demo( + work, + kind, + v, + lambda b=body: api("POST", "/time_entries.json", {"time_entry": b}), + ) bot_id = one("SELECT id FROM users WHERE login=%s", (bot["login"],)) report = propose(work, {"bot_user_id": str(bot_id)}) print("questions:", len(report["questions"])) + db.close() fx.reset() diff --git a/packages/flow/benchmark/generate_contracts/summarize.py b/packages/flow/benchmark/generate_contracts/summarize.py new file mode 100644 index 0000000000..efe88d438c --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/summarize.py @@ -0,0 +1,81 @@ +"""Count a validation run: correct writes confirmed, faults stopped, by kind. + + python benchmark/generate_contracts/summarize.py APP WORKDIR + +A case passes when the verifier says CONFIRMED. The oracle's label says what +the write really did: ``correct`` (or no write needed), or harm. A correct +write that is not CONFIRMED is a false stop; a harm that is CONFIRMED is a +miss. Fault scope (in or out of what the verifier claims) comes from the +pack's ``faults.yaml``. +""" + +from __future__ import annotations + +import json +import sys +from collections import defaultdict +from pathlib import Path + +import yaml +from demo_lib import FCG + +GOOD = { + "correct" +} # "nothing" (no write where one was needed) and "harm" must be stopped + + +def main() -> None: + app, work = sys.argv[1], Path(sys.argv[2]) + scope = { + f["id"]: f.get("scope", "in") + for f in yaml.safe_load( + (FCG / "apps" / app / "faults" / "faults.yaml").read_text() + ) + } + rows = [ + json.loads(x) + for x in (work / "validate.jsonl").read_text().splitlines() + if x.strip() + ] + by_kind: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int)) + misses, false_stops = [], [] + for r in rows: + k = r["kind"] + ok = r["verdict"]["outcome"] == "CONFIRMED" + label = str(r["oracle"].get("label")) + harm = label not in GOOD + sc = scope.get(r["case"].split("-", 1)[1], "in") + if harm: + by_kind[k][f"harm_{sc}"] += 1 + if ok: + by_kind[k][f"harm_{sc}_confirmed"] += 1 + misses.append((r["case"], sc, label)) + else: + by_kind[k]["correct"] += 1 + if ok: + by_kind[k]["correct_confirmed"] += 1 + else: + false_stops.append( + ( + r["case"], + r["verdict"]["outcome"], + "; ".join(r["verdict"]["reasons"])[:160], + ) + ) + print( + "| Kind | Correct writes confirmed | In-scope harms stopped | Out-of-scope harms stopped |" + ) + print("| --- | --- | --- | --- |") + for k, c in sorted(by_kind.items()): + hi, ho = c["harm_in"], c["harm_out"] + print(f"| `{k}` | {c['correct_confirmed']}/{c['correct']} | {hi - c['harm_in_confirmed']}/{hi} | " + f"{ho - c['harm_out_confirmed']}/{ho} |") # fmt: skip + print() + for m in misses: + print("MISS", *m) + for f in false_stops: + print("FALSE STOP", *f) + + +if __name__ == "__main__": + main() diff --git a/packages/flow/benchmark/generate_contracts/validate.py b/packages/flow/benchmark/generate_contracts/validate.py index 65f7eedab3..d7371955bb 100644 --- a/packages/flow/benchmark/generate_contracts/validate.py +++ b/packages/flow/benchmark/generate_contracts/validate.py @@ -23,7 +23,6 @@ from typing import Any import yaml - from demo_lib import FCG, fixture, load from openadapt_flow.generate import verify_contract as gen @@ -37,7 +36,9 @@ def main() -> int: ap.add_argument("app") ap.add_argument("work", type=Path) ap.add_argument("--only", nargs="*") - ap.add_argument("--actor-id", required=True, help="the bot's id as the contracts bind it") + ap.add_argument( + "--actor-id", required=True, help="the bot's id as the contracts bind it" + ) ap.add_argument("--actor-param", default="bot_user_id") ap.add_argument("--no-faults", action="store_true") ap.add_argument("--no-correct", action="store_true") @@ -52,11 +53,20 @@ def main() -> int: out = args.work / "validate.jsonl" done = set() if out.exists(): - done = {json.loads(x)["case"] for x in out.read_text().splitlines() if x.strip()} + done = { + json.loads(x)["case"] for x in out.read_text().splitlines() if x.strip() + } cases: list[tuple[str, dict, Any, float]] = [] if not args.no_correct: for tid, task in tasks.items(): - cases.append((f"correct-{tid}", task, lambda t=task: faults.correct_write(t, fx.bot()), 0.0)) + cases.append( + ( + f"correct-{tid}", + task, + lambda t=task: faults.correct_write(t, fx.bot()), + 0.0, + ) + ) if not args.no_faults: for f in yaml.safe_load((app_dir / "faults" / "faults.yaml").read_text()): task = tasks[f["task"]] @@ -70,7 +80,9 @@ def main() -> int: ) # fmt: skip for case, task, act, late in cases: short = case.split("-", 1)[1] - if case in done or (args.only and short not in args.only and task["id"] not in args.only): + if case in done or ( + args.only and short not in args.only and task["id"] not in args.only + ): continue while os.getloadavg()[0] >= 35: time.sleep(30) @@ -79,17 +91,28 @@ def main() -> int: before = oracle.snapshot() pending = None try: - contract, params = gen.choose(manifest, task["kind"], task["values"], {args.actor_param: args.actor_id}) + contract, params = gen.choose( + manifest, + task["kind"], + task["values"], + {args.actor_param: args.actor_id}, + ) rec["contract"] = contract.name tmp = tempfile.mkdtemp() pending = begin_write(contract, params, actor=Actor("agent", "generated-check"), ledger=None, write_key=f"{case}-{time.time_ns()}", workdir=tmp) # fmt: skip except Exception as exc: # noqa: BLE001 - a stop before the write - rec["verdict"] = {"outcome": "BLOCKED_AT_BEGIN", "reasons": [f"{type(exc).__name__}: {exc}"]} + rec["verdict"] = { + "outcome": "BLOCKED_AT_BEGIN", + "reasons": [f"{type(exc).__name__}: {exc}"], + } try: rec["claim"] = act() except Exception as exc: # noqa: BLE001 - rec["claim"] = {"status": "not_done", "note": f"{type(exc).__name__}: {exc}"} + rec["claim"] = { + "status": "not_done", + "note": f"{type(exc).__name__}: {exc}", + } if pending is not None: try: r = verify_write(pending) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index a166aa5141..02d8bdba7d 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -347,7 +347,12 @@ def locate( added = set() for ch in demo.diff.changes: t2 = schema.tables.get(ch.table) - if ch.kind == "added" and ch.after and t2 is not None and len(t2.primary_key) == 1: + if ( + ch.kind == "added" + and ch.after + and t2 is not None + and len(t2.primary_key) == 1 + ): added.add((ch.table, ch.after.get(t2.primary_key[0]))) def walk( @@ -393,7 +398,9 @@ def walk( for col in child.columns: if ch.after.get(col.name) != key: continue - if links is not None and col.name not in links.get(ch.table, {col.name}): + if links is not None and col.name not in links.get( + ch.table, {col.name} + ): continue declared = any( fk.column == col.name and fk.table == t.name @@ -405,7 +412,11 @@ def walk( child, ch.after, hops - + (Hop("child", ch.table, col.name, t.primary_key[0], declared),), + + ( + Hop( + "child", ch.table, col.name, t.primary_key[0], declared + ), + ), depth + 1, ) @@ -416,7 +427,9 @@ def walk( return sorted(unique.values(), key=_rank) -def link_columns(schema: Schema, demos: Sequence[tuple["Demo", RowChange]]) -> dict[str, set[str]]: +def link_columns( + schema: Schema, demos: Sequence[tuple["Demo", RowChange]] +) -> dict[str, set[str]]: """Per table, the columns that hold the main record's key in every row that table gained, across all demonstrations (a journal's ``journalized_id``, not its ``user_id`` that happens to be equal once).""" @@ -537,10 +550,14 @@ def column_roles( if col.auto or (name in t.primary_key and len(t.primary_key) == 1): out[name] = ColumnRole("key") continue - if col.kind in ("date", "datetime") and all( - v is None or _within(v, d.started_at, d.finished_at) - for (d, _), v in zip(rows, values) - ) and any(v is not None for v in values): + if ( + col.kind in ("date", "datetime") + and all( + v is None or _within(v, d.started_at, d.finished_at) + for (d, _), v in zip(rows, values) + ) + and any(v is not None for v in values) + ): out[name] = ColumnRole("timestamp") continue actor = [ @@ -553,17 +570,22 @@ def column_roles( continue links = link_values or [set() for _ in rows] if values and all( - v is not None and (v in lv or (len(t.primary_key) == 1 and v == r.get(t.primary_key[0]))) + v is not None + and (v in lv or (len(t.primary_key) == 1 and v == r.get(t.primary_key[0]))) for v, lv, (_, r) in zip(values, links, rows) ): out[name] = ColumnRole("link") continue - if len(t.primary_key) == 1 and values and all( - v is not None - and r.get(t.primary_key[0]) is not None - and str(r.get(t.primary_key[0])) in v - and v != r.get(t.primary_key[0]) - for v, (_, r) in zip(values, rows) + if ( + len(t.primary_key) == 1 + and values + and all( + v is not None + and r.get(t.primary_key[0]) is not None + and str(r.get(t.primary_key[0])) in v + and v != r.get(t.primary_key[0]) + for v, (_, r) in zip(values, rows) + ) ): out[name] = ColumnRole("varies", detail={"why": "holds the row's own key"}) continue @@ -585,7 +607,11 @@ def column_roles( out[name] = ColumnRole("constant", None) else: out[name] = ColumnRole( - "constant", v, detail={"is_default": default is not None and default.strip("'") == v} + "constant", + v, + detail={ + "is_default": default is not None and default.strip("'") == v + }, ) continue out[name] = ColumnRole("varies") diff --git a/packages/flow/openadapt_flow/generate/sql_store.py b/packages/flow/openadapt_flow/generate/sql_store.py index df0a7d666c..7feeede142 100644 --- a/packages/flow/openadapt_flow/generate/sql_store.py +++ b/packages/flow/openadapt_flow/generate/sql_store.py @@ -181,7 +181,9 @@ def connector(read: Mapping[str, Any]) -> Callable[[], Any]: env = read.get("sql_password_env") if env: if env not in os.environ: - raise StoreError(f"environment variable {env} (the read password) is not set") + raise StoreError( + f"environment variable {env} (the read password) is not set" + ) args["password"] = os.environ[str(env)] def connect() -> Any: @@ -253,7 +255,9 @@ def _mysql_schema(cur: Any) -> Schema: t.columns.append( Column( name=str(cname), - data_type="tinyint(1)" if ctype.startswith("tinyint(1)") else str(dtype).lower(), + data_type="tinyint(1)" + if ctype.startswith("tinyint(1)") + else str(dtype).lower(), nullable=str(nullable).upper() == "YES", default=None if default is None else str(default), auto="auto_increment" in extra, @@ -438,9 +442,7 @@ def save_snapshot(path: Path, rows: Mapping[str, TableRows]) -> None: def load_snapshot(path: Path) -> dict[str, TableRows]: data = json.loads(path.read_text(encoding="utf-8")) - return { - n: TableRows(dict(t["rows"]), t.get("count_only")) for n, t in data.items() - } + return {n: TableRows(dict(t["rows"]), t.get("count_only")) for n, t in data.items()} @dataclass @@ -476,7 +478,9 @@ def tables(self) -> list[str]: def of(self, table: str, kind: Optional[str] = None) -> list[RowChange]: return [ - c for c in self.changes if c.table == table and (kind is None or c.kind == kind) + c + for c in self.changes + if c.table == table and (kind is None or c.kind == kind) ] def to_json(self) -> dict[str, Any]: @@ -489,7 +493,10 @@ def to_json(self) -> dict[str, Any]: def from_json(cls, data: Mapping[str, Any]) -> "Diff": return cls( [RowChange(**c) for c in data.get("changes", [])], - {k: (int(v[0]), int(v[1])) for k, v in dict(data.get("unread", {})).items()}, + { + k: (int(v[0]), int(v[1])) + for k, v in dict(data.get("unread", {})).items() + }, ) @@ -501,7 +508,10 @@ def diff(before: Mapping[str, TableRows], after: Mapping[str, TableRows]) -> Dif for name in sorted(set(before) | set(after)): a, b = before.get(name), after.get(name) if a is None or b is None: - unread[name] = (0 if a is None else len(a.rows), 0 if b is None else len(b.rows)) + unread[name] = ( + 0 if a is None else len(a.rows), + 0 if b is None else len(b.rows), + ) continue if a.count_only is not None or b.count_only is not None: ca = a.count_only if a.count_only is not None else len(a.rows) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 1943708792..0342476c1e 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -47,11 +47,11 @@ from openadapt_flow.generate import sql_store as store from openadapt_flow.generate.demo_analysis import ( - _rank, ColumnRole, Demo, Hop, Location, + _rank, best, column_roles, is_identifier, @@ -59,9 +59,8 @@ locate, looks_like_log, pick_anchor, - snake, ) -from openadapt_flow.generate.sql_store import RowChange, Schema, Table +from openadapt_flow.generate.sql_store import RowChange, Schema MANIFEST_SCHEMA = "openadapt.contract-manifest/v1" #: Most contracts one kind gets (all subsets of up to six optional fields). @@ -128,7 +127,10 @@ def load_kinds(path: Path) -> list[KindSpec]: [str(p) for p in k["parameters"]], str(k.get("description") or ""), [ - {"instruction": e.get("instruction", ""), "values": dict(e.get("values") or {})} + { + "instruction": e.get("instruction", ""), + "values": dict(e.get("values") or {}), + } for e in k.get("examples") or [] ], ) @@ -234,7 +236,9 @@ class KindPlan: def _scalars(values: Mapping[str, Any]) -> dict[str, Any]: return { - k: v for k, v in values.items() if v is not None and not isinstance(v, (list, dict)) + k: v + for k, v in values.items() + if v is not None and not isinstance(v, (list, dict)) } @@ -286,7 +290,11 @@ def analyze( plan.anchor, plan.mode = anchor.table, mode plan.demos.append(d) plan.anchors.append(anchor) - lists = [p for p in spec.parameters if any(isinstance(d.values.get(p), list) for d in plan.demos)] + lists = [ + p + for p in spec.parameters + if any(isinstance(d.values.get(p), list) for d in plan.demos) + ] links = link_columns(schema, list(zip(plan.demos, plan.anchors))) scalars = [p for p in spec.parameters if p not in lists] @@ -405,7 +413,12 @@ def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: col = schema.tables[plan.anchor].column(name) if col is None or name in schema.tables[plan.anchor].primary_key: return None - rule = {"number": "number", "bool": "number", "date": "date", "datetime": "datetime"}.get(col.kind, "exact") + rule = { + "number": "number", + "bool": "number", + "date": "date", + "datetime": "datetime", + }.get(col.kind, "exact") if col.kind not in ("text", "number", "bool", "date", "datetime"): return None return Location((), (name,), rule) @@ -428,7 +441,13 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: key = (a.after or {}).get(loc.hops[0].ref_column) if ch.after.get(loc.hops[0].column) != key: continue - if locate(schema, d, _as_change(ch.table, ch.after), d.values[p], max_hops=len(rel.hops)): + if locate( + schema, + d, + _as_change(ch.table, ch.after), + d.values[p], + max_hops=len(rel.hops), + ): rows.append((d, ch.after)) break explained = {loc.hops[0].column, *rel.columns} @@ -438,9 +457,12 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: if q == p or not rows: continue hits = [ - locate(schema, d, _as_change(loc.hops[0].table, r), d.values[q], max_hops=1) + locate( + schema, d, _as_change(loc.hops[0].table, r), d.values[q], max_hops=1 + ) for d, r in rows - if d.values.get(q) is not None and not isinstance(d.values.get(q), (list, dict)) + if d.values.get(q) is not None + and not isinstance(d.values.get(q), (list, dict)) ] if hits and all(h and h[0].hops for h in hits): co.append(q) @@ -461,7 +483,11 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: items = d.values.get(p) if not isinstance(items, list): continue - key = (a.after or {}).get(schema.tables[plan.anchor].primary_key[0]) if schema.tables[plan.anchor].primary_key else None + key = ( + (a.after or {}).get(schema.tables[plan.anchor].primary_key[0]) + if schema.tables[plan.anchor].primary_key + else None + ) children = [ ch for ch in d.diff.changes @@ -480,14 +506,14 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: if i in used: continue locs = { - k: locate(schema, d, _as_change(ch.table, ch.after), v, max_hops=1) + k: locate( + schema, d, _as_change(ch.table, ch.after), v, max_hops=1 + ) for k, v in item.items() } if all(locs.values()): used.add(i) - link_col = next( - c for c, v in ch.after.items() if v == key - ) + link_col = next(c for c, v in ch.after.items() if v == key) table = ch.table link = Hop( "child", @@ -514,7 +540,11 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: explained = {link.column} | {c for loc in locs.values() for c in loc.columns} explained |= {loc.hops[0].column for loc in locs.values() if loc.hops} plan.lists[p] = ChildGroup( - table, link, locs, column_roles(schema, table, rows, explained, {}), list_param=p + table, + link, + locs, + column_roles(schema, table, rows, explained, {}), + list_param=p, ) @@ -544,17 +574,27 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N plan.removed[ch.table] = plan.removed.get(ch.table, 0) + 1 continue if ch.kind == "changed": - linked = ch.table in child_tables and a.after is not None and any( - v == (a.after or {}).get(plan_pk(schema, plan.anchor)) - for v in (ch.after or {}).values() + linked = ( + ch.table in child_tables + and a.after is not None + and any( + v == (a.after or {}).get(plan_pk(schema, plan.anchor)) + for v in (ch.after or {}).values() + ) ) if linked: g = plan.children[child_tables[ch.table]] - g.changed_columns = sorted(set(g.changed_columns) | set(ch.changed_columns)) + g.changed_columns = sorted( + set(g.changed_columns) | set(ch.changed_columns) + ) elif ch.table == plan.anchor: - plan.bookkeeping.setdefault(ch.table, set()).update(ch.changed_columns) + plan.bookkeeping.setdefault(ch.table, set()).update( + ch.changed_columns + ) else: - plan.bookkeeping.setdefault(ch.table, set()).update(ch.changed_columns) + plan.bookkeeping.setdefault(ch.table, set()).update( + ch.changed_columns + ) continue # added if ch.table in list_tables: @@ -574,24 +614,23 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N d.__dict__["_added_keys"] = added_keys for t, cs in sorted(counts.items()): rows = [r for group in pure.get(t, []) for r in group] - if ( - len(set(cs)) == 1 - and cs[0] > 0 - and not looks_like_log(t) - and rows - ): + if len(set(cs)) == 1 and cs[0] > 0 and not looks_like_log(t) and rows: links = [getattr(d, "_added_keys", set()) for d, _ in rows] roles = column_roles(schema, t, rows, set(), actor_values, links) - plan.sides.append( - SideSpec(t, cs[0], roles, unless=child_tables.get(t)) - ) + plan.sides.append(SideSpec(t, cs[0], roles, unless=child_tables.get(t))) elif rows: plan.logs.add(t) if plan.mode == "update": for a in plan.anchors: plan.anchor_changed |= set(a.changed_columns) - explained = {c for loc in plan.locations.values() if not loc.hops for c in loc.columns} - explained |= {loc.hops[0].column for loc in plan.locations.values() if loc.hops and loc.hops[0].kind == "fk"} + explained = { + c for loc in plan.locations.values() if not loc.hops for c in loc.columns + } + explained |= { + loc.hops[0].column + for loc in plan.locations.values() + if loc.hops and loc.hops[0].kind == "fk" + } for locs in plan.dispatch_locations.values(): for loc in locs.values(): explained |= set(loc.columns) if not loc.hops else {loc.hops[0].column} @@ -610,7 +649,13 @@ def _is_param_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> boo if g.table != ch.table or d.values.get(p) is None or ch.after is None: continue rel = g.params[p] - if locate(schema, d, _as_change(ch.table, ch.after), d.values[p], max_hops=len(rel.hops)): + if locate( + schema, + d, + _as_change(ch.table, ch.after), + d.values[p], + max_hops=len(rel.hops), + ): return True return False @@ -626,8 +671,14 @@ def _is_ref_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: if not rel.hops or rel.hops[0].table != ch.table or d.values.get(p) is None: continue col = rel.columns[0] if len(rel.columns) == 1 else None - if col and ch.after.get(col) is not None and _rule_ok(ch.after.get(col), d.values[p], rel.rule): - plan.added_refs.setdefault(p, {"table": ch.table, "column": col, "rule": rel.rule}) + if ( + col + and ch.after.get(col) is not None + and _rule_ok(ch.after.get(col), d.values[p], rel.rule) + ): + plan.added_refs.setdefault( + p, {"table": ch.table, "column": col, "rule": rel.rule} + ) return True return False @@ -760,7 +811,9 @@ def empty(self, alias: str, table: str, column: str) -> str: return f"CASE WHEN {ref} IS NULL OR {ref} = '' THEN '1' ELSE '0' END" return f"CASE WHEN {ref} IS NULL THEN '1' ELSE '0' END" - def copied(self, alias: str, detail: Mapping[str, Any], table: str, column: str) -> str: + def copied( + self, alias: str, detail: Mapping[str, Any], table: str, column: str + ) -> str: src = ( f"(SELECT s.{self.q(detail['column'])} FROM {self.q(detail['table'])} s " f"WHERE s.{self.q(detail['ref_column'])} = {alias}.{self.q(detail['via'])})" @@ -819,12 +872,16 @@ class Variant: fields: list[str] lists: dict[str, int] = field(default_factory=dict) dispatch_value: Optional[str] = None - files: dict[str, str] = field(default_factory=dict) # create | found | update -> path + files: dict[str, str] = field( + default_factory=dict + ) # create | found | update -> path sample: dict[str, Any] = field(default_factory=dict) @property def slug(self) -> str: - parts = sorted(self.fields) + [f"{k}x{n}" for k, n in sorted(self.lists.items())] + parts = sorted(self.fields) + [ + f"{k}x{n}" for k, n in sorted(self.lists.items()) + ] if self.dispatch_value is not None: parts.insert(0, f"{self.dispatch_value}") text = "+".join(parts) @@ -872,7 +929,9 @@ def build_contract( named_tables: set[str] = {anchor} # The main record, with every column and the values its parameters reach. - main = Surface("record", settings.views.get(anchor, anchor), _base_columns(b, anchor)) + main = Surface( + "record", settings.views.get(anchor, anchor), _base_columns(b, anchor) + ) match: dict[str, Any] = {} locations = dict(plan.locations) if variant.dispatch_value is not None: @@ -939,7 +998,8 @@ def build_contract( for ip in identity_params: loc = locations[ip] up = Location( - (Hop("fk", anchor, link.column, link.ref_column, True),) + loc.hops, + (Hop("fk", anchor, link.column, link.ref_column, True),) + + loc.hops, loc.columns, loc.rule, ) @@ -947,7 +1007,9 @@ def build_contract( if loc.rule in ("number", "date", "datetime", "minutes"): expr = b.flag(expr, loc.rule, b.ph(ip)) cols.append((f"main__{_alias(ip)}", expr)) - surfaces[sname] = Surface(sname, settings.views.get(g.table, g.table), cols) + surfaces[sname] = Surface( + sname, settings.views.get(g.table, g.table), cols + ) named_tables.add(g.table) surf = surfaces[sname] items = range(variant.lists[p]) if g.list_param else [None] @@ -993,7 +1055,19 @@ def build_contract( for p, ref in plan.added_refs.items(): if p not in fields: continue - effects.append(_raw_effect(b, schema, surfaces, settings, ref["table"], {ref["column"]: _value_expr(p, ref["rule"])}, 1, {}, f"the new {ref['table']} row holds {p}")) + effects.append( + _raw_effect( + b, + schema, + surfaces, + settings, + ref["table"], + {ref["column"]: _value_expr(p, ref["rule"])}, + 1, + {}, + f"the new {ref['table']} row holds {p}", + ) + ) for side in plan.sides: if side.unless is not None and side.unless in fields: continue @@ -1004,7 +1078,17 @@ def build_contract( elif role.role == "actor": m[col] = {"param": role.detail["param"]} effects.append( - _raw_effect(b, schema, surfaces, settings, side.table, m, side.count, side.roles, f"{side.count} row(s) the app adds to {side.table}") + _raw_effect( + b, + schema, + surfaces, + settings, + side.table, + m, + side.count, + side.roles, + f"{side.count} row(s) the app adds to {side.table}", + ) ) verifier: dict[str, Any] = {"kind": "surfaces", "surfaces": {}} @@ -1016,7 +1100,10 @@ def build_contract( verifier["surfaces"][s.name] = sub # Main and child effects read named surfaces: no other read is judged # against them, and the whole-database frame covers every other table. - ignore = sorted({c for cols in bookkeeping.values() for c in cols} & _all_columns(schema, named_tables)) + ignore = sorted( + {c for cols in bookkeeping.values() for c in cols} + & _all_columns(schema, named_tables) + ) frame: dict[str, Any] = {} if ignore: frame["ignore_fields"] = ignore @@ -1056,7 +1143,9 @@ def build_contract( } if nrf: db["new_row_fields"] = nrf - ign = {t: sorted(c) for t, c in bookkeeping.items() if t not in named_tables and c} + ign = { + t: sorted(c) for t, c in bookkeeping.items() if t not in named_tables and c + } if ign: db["ignore_columns"] = ign if settings.recheck_after_s: @@ -1070,9 +1159,15 @@ def build_contract( continue if f"raw_{_alias(t)}" in surfaces: continue - s = Surface(f"raw_{_alias(t)}", settings.views.get(t, t), _base_columns(b, t)) + s = Surface( + f"raw_{_alias(t)}", settings.views.get(t, t), _base_columns(b, t) + ) surfaces[s.name] = s - verifier["surfaces"][s.name] = {"kind": "sql", **settings.read, "sql_query": s.sql(b)} + verifier["surfaces"][s.name] = { + "kind": "sql", + **settings.read, + "sql_query": s.sql(b), + } doc: dict[str, Any] = { "schema_version": "openadapt.verify/v1", "environment_id": settings.environment_id, @@ -1085,13 +1180,19 @@ def build_contract( def _all_columns(schema: Schema, tables: set[str]) -> set[str]: - return {c.name for t in tables if t in schema.tables for c in schema.tables[t].columns} + return { + c.name for t in tables if t in schema.tables for c in schema.tables[t].columns + } def _place(b: SqlBuilder, surf: Surface, table: str, param: str, loc: Location) -> str: """Add the column that holds ``param``'s value to ``surf``; its name.""" - if not loc.hops and len(loc.columns) == 1 and loc.rule in ("exact", "email", "phone"): + if ( + not loc.hops + and len(loc.columns) == 1 + and loc.rule in ("exact", "email", "phone") + ): return loc.columns[0] if loc.columns[0] != "id" else "id" expr = b.path("t", table, loc) alias = f"p__{_alias(param)}" @@ -1158,8 +1259,26 @@ def _raw_effect( for col, value in list(m.items()): c = t.column(col) lit = value.get("literal") if isinstance(value, dict) else None - if c is not None and lit is not None and not ( - c.kind == "text" or (c.kind in ("number", "bool") and c.data_type.startswith(("int", "tinyint", "smallint", "mediumint", "bigint", "integer")) and re.fullmatch(r"-?\d+", lit)) + if ( + c is not None + and lit is not None + and not ( + c.kind == "text" + or ( + c.kind in ("number", "bool") + and c.data_type.startswith( + ( + "int", + "tinyint", + "smallint", + "mediumint", + "bigint", + "integer", + ) + ) + and re.fullmatch(r"-?\d+", lit) + ) + ) ): m.pop(col) nrf = set(getattr(surf, "_new_row_fields", set())) @@ -1169,8 +1288,20 @@ def _raw_effect( role = roles.get(c.name) if role is not None and role.role == "constant" and role.value is None: continue - if role is None or role.role != "constant" or (role.detail or {}).get("is_default"): - if role is None or role.role in ("key", "timestamp", "link", "varies", "copied", "actor", "free"): + if ( + role is None + or role.role != "constant" + or (role.detail or {}).get("is_default") + ): + if role is None or role.role in ( + "key", + "timestamp", + "link", + "varies", + "copied", + "actor", + "free", + ): nrf.add(c.name) continue nrf.add(c.name) @@ -1221,7 +1352,9 @@ def _anchor_roles( # -- variants ----------------------------------------------------------------------- -def variants(schema: Schema, plan: KindPlan, spec: KindSpec, answers: Mapping[str, Any]) -> list[Variant]: +def variants( + schema: Schema, plan: KindPlan, spec: KindSpec, answers: Mapping[str, Any] +) -> list[Variant]: out: list[Variant] = [] lists = list(plan.lists) if plan.mode == "update": @@ -1239,7 +1372,10 @@ def variants(schema: Schema, plan: KindPlan, spec: KindSpec, answers: Mapping[st vp = next(iter(next(iter(values.values())))) for c in schema.tables[plan.anchor].columns: for name in {c.name, _camel(c.name)}: - if name in values or c.name in schema.tables[plan.anchor].primary_key: + if ( + name in values + or c.name in schema.tables[plan.anchor].primary_key + ): continue if c.kind == "text": values[name] = {vp: Location((), (c.name,), "exact")} @@ -1342,7 +1478,11 @@ def dry_read(path: Path, params: Mapping[str, Any], mysql: bool) -> Optional[str would. None when it reads; else why not.""" from openadapt_flow.verifier import Contract, VerifierError - from openadapt_flow.verify_run import FRAME_DB_DETAIL, VerifyRunError, capture_baseline + from openadapt_flow.verify_run import ( + FRAME_DB_DETAIL, + VerifyRunError, + capture_baseline, + ) try: contract = Contract.load(path) @@ -1365,7 +1505,10 @@ def dry_read(path: Path, params: Mapping[str, Any], mysql: bool) -> Optional[str if mysql: snap = detail.get(FRAME_DB_DETAIL) if not isinstance(snap, dict): - from openadapt_flow.runtime.effects.db_snapshot import mac_key, take_snapshot + from openadapt_flow.runtime.effects.db_snapshot import ( + mac_key, + take_snapshot, + ) from openadapt_flow.verify_run import _audit_sql, _db_connect, _ref_columns database = contract.config.frame.database @@ -1420,7 +1563,9 @@ def check_read_only(connect: Any, dialect: str) -> list[str]: "SUPER", "EVENT", ): - if re.search(rf"\b{word}\b", privs) and not privs.startswith("USAGE"): + if re.search(rf"\b{word}\b", privs) and not privs.startswith( + "USAGE" + ): problems.append(f"the read account has {word}: {grant}") else: cur.execute( @@ -1429,7 +1574,9 @@ def check_read_only(connect: Any, dialect: str) -> list[str]: ) row = cur.fetchone() if row and any(row): - problems.append("the read account is a superuser or can create roles or databases") + problems.append( + "the read account is a superuser or can create roles or databases" + ) cur.execute( "SELECT table_name FROM information_schema.role_table_grants " "WHERE grantee = current_user AND privilege_type IN " @@ -1461,7 +1608,9 @@ def _admin_connect(admin_read: Mapping[str, Any]) -> Any: env = admin_read.get("sql_password_env") if env: if env not in os.environ: - raise GenerateError(f"environment variable {env} (the admin password) is not set") + raise GenerateError( + f"environment variable {env} (the admin password) is not set" + ) args["password"] = os.environ[str(env)] return module.connect(**args) @@ -1519,7 +1668,9 @@ def create_views( if schema.dialect == "mysql": cur.execute(f"GRANT SELECT ON {v} TO {reader}") else: - cur.execute(f"GRANT SELECT ON {v} TO {store.quote('postgresql', reader)}") + cur.execute( + f"GRANT SELECT ON {v} TO {store.quote('postgresql', reader)}" + ) made[t] = view conn.commit() finally: @@ -1560,7 +1711,9 @@ def propose( if dialect == "mysql" and not _sees_triggers(connect): if grant_trigger_flag: if not admin_read or not reader_account: - raise GenerateError("--grant-trigger needs --admin-read and --reader-account") + raise GenerateError( + "--grant-trigger needs --admin-read and --reader-account" + ) applied.append(grant_trigger(admin_read, schema, reader_account)) if not _sees_triggers(connect): raise GenerateError( @@ -1599,7 +1752,9 @@ def propose( parts = [] for name, tied in asked.items(): if tied: - opts = "; ".join(f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied)) + opts = "; ".join( + f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied) + ) parts.append(f"`{name}` is in several places: {opts}") else: parts.append( @@ -1609,7 +1764,8 @@ def propose( location_questions.append( Question( f"locations.{p.kind}", - f"Kind `{p.kind}`: " + " ".join(parts) + f"Kind `{p.kind}`: " + + " ".join(parts) + f" Answer `locations.{p.kind}: {{: }}` to pick one.", None, ) @@ -1641,26 +1797,35 @@ def propose( lines.append( "Tables treated as the app's own logs (rows may be added, existing " "rows must not change" - + (")" if dialect == "mysql" else "; on PostgreSQL they are not read at all)") + + ( + ")" + if dialect == "mysql" + else "; on PostgreSQL they are not read at all)" + ) + ": " + ", ".join(sorted(logs)) ) if book: lines.append( "Columns that change on their own and are not compared: " - + "; ".join(f"{t}: {', '.join(sorted(c))}" for t, c in sorted(book.items()) if c) + + "; ".join( + f"{t}: {', '.join(sorted(c))}" for t, c in sorted(book.items()) if c + ) ) if unchecked: lines.append( "Columns of a new record that differ between demonstrations and are " "not checked: " - + "; ".join(f"{t}: {', '.join(sorted(c))}" for t, c in sorted(unchecked.items())) + + "; ".join( + f"{t}: {', '.join(sorted(c))}" for t, c in sorted(unchecked.items()) + ) ) questions.append( Question( "bookkeeping", "Is this the app's own bookkeeping, which a write may change without " - "a task asking? " + " ".join(lines) + "a task asking? " + + " ".join(lines) + " Answer `bookkeeping: accept`, or list changes under " "logs_add, logs_remove, columns_add, columns_remove.", "accept", @@ -1682,10 +1847,13 @@ def propose( ) ) questions.extend(location_questions) - questions_open = [ - q for q in questions if _answer(answers, q.key) is None - ] - settings = Settings(read, dict(actor_params), recheck or (DEFAULT_RECHECK_S if dialect == "mysql" else None), env) + questions_open = [q for q in questions if _answer(answers, q.key) is None] + settings = Settings( + read, + dict(actor_params), + recheck or (DEFAULT_RECHECK_S if dialect == "mysql" else None), + env, + ) # Tables whose name the SQL guard refuses: a view, or a refusal. blocked = sorted( @@ -1695,14 +1863,21 @@ def propose( ) needed = sorted( {p.anchor for p in plans if p.anchor in blocked} - | {g.table for p in plans for g in list(p.children.values()) + list(p.lists.values()) if g.table in blocked} + | { + g.table + for p in plans + for g in list(p.children.values()) + list(p.lists.values()) + if g.table in blocked + } ) if needed: existing = {t: f"oa_read_{t}" for t in needed if f"oa_read_{t}" in schema.views} missing = [t for t in needed if t not in existing] if missing and create_views_flag: if not admin_read or not reader_account: - raise GenerateError("--create-views needs --admin-read and --reader-account") + raise GenerateError( + "--create-views needs --admin-read and --reader-account" + ) existing.update(create_views(admin_read, schema, missing, reader_account)) schema = store.read_schema(connect, dialect) for t, v in list(existing.items()): @@ -1710,7 +1885,9 @@ def propose( existing.pop(t) settings.views = existing for p in plans: - tables = {p.anchor} | {g.table for g in list(p.children.values()) + list(p.lists.values())} + tables = {p.anchor} | { + g.table for g in list(p.children.values()) + list(p.lists.values()) + } for t in sorted(tables & set(blocked)): if t not in existing: p.refused.append( @@ -1733,7 +1910,13 @@ def propose( "dialect": dialect, "tables": len(schema.tables), "questions": [ - {"number": i + 1, "key": q.key, "text": q.text, "proposal": q.proposal, "answered": _answer(answers, q.key) is not None} + { + "number": i + 1, + "key": q.key, + "text": q.text, + "proposal": q.proposal, + "answered": _answer(answers, q.key) is not None, + } for i, q in enumerate(questions) ], "kinds": {}, @@ -1751,15 +1934,23 @@ def propose( rep: dict[str, Any] = { "mode": p.mode, "main_table": p.anchor, - "locations": {k: v.describe(p.anchor) for k, v in sorted(p.locations.items())}, + "locations": { + k: v.describe(p.anchor) for k, v in sorted(p.locations.items()) + }, "placed_by_name": sorted(p.by_name), "demonstrations": len(p.demos), "children": {k: g.table for k, g in p.children.items()}, "lists": {k: g.table for k, g in p.lists.items()}, - "side_rows": [{"table": s.table, "count": s.count, "unless": s.unless} for s in p.sides], + "side_rows": [ + {"table": s.table, "count": s.count, "unless": s.unless} + for s in p.sides + ], "logs": sorted(p.logs), "refused": list(p.refused), - "unresolved": {k: [loc.describe(p.anchor) for loc in v] for k, v in sorted(p.unresolved.items())}, + "unresolved": { + k: [loc.describe(p.anchor) for loc in v] + for k, v in sorted(p.unresolved.items()) + }, "emitted": 0, "rejected": [], } @@ -1774,7 +1965,13 @@ def propose( continue flavors = ["create", "found"] if p.mode == "create" else ["update"] for v in variants(schema, p, spec, answers): - if any(f not in p.locations and f != p.dispatch and f not in p.children and f not in (p.dispatch_locations.get(v.dispatch_value or "", {})) for f in v.fields): + if any( + f not in p.locations + and f != p.dispatch + and f not in p.children + and f not in (p.dispatch_locations.get(v.dispatch_value or "", {})) + for f in v.fields + ): continue ok = True for flavor in flavors: @@ -1787,15 +1984,25 @@ def propose( f"{flavor}, fields {', '.join(v.fields + [f'{k}x{n}' for k, n in v.lists.items()])}.\n" "# Proposed, not admitted: a person answers QUESTIONS.md first.\n" ) - path.write_text(header + yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=100), encoding="utf-8") + path.write_text( + header + + yaml.safe_dump( + doc, sort_keys=False, allow_unicode=True, width=100 + ), + encoding="utf-8", + ) why = None for s in doc["verifier"]["surfaces"].values(): why = why or guard_problem(s["sql_query"]) if why is None and validate: - why = dry_read(path, flatten(sample_values(p, v)), dialect == "mysql") + why = dry_read( + path, flatten(sample_values(p, v)), dialect == "mysql" + ) if why is not None: path.unlink() - rep["rejected"].append({"variant": v.slug, "flavor": flavor, "why": why}) + rep["rejected"].append( + {"variant": v.slug, "flavor": flavor, "why": why} + ) ok = False break v.files[flavor] = str(rel) @@ -1814,27 +2021,41 @@ def propose( (out / f).unlink(missing_ok=True) manifest["kinds"][p.kind] = entry report["kinds"][p.kind] = rep - (out / "manifest.yaml").write_text(yaml.safe_dump(manifest, sort_keys=False, allow_unicode=True), encoding="utf-8") - (out / "report.json").write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8") - (out / "QUESTIONS.md").write_text(_questions_md(questions, answers, report), encoding="utf-8") + (out / "manifest.yaml").write_text( + yaml.safe_dump(manifest, sort_keys=False, allow_unicode=True), encoding="utf-8" + ) + (out / "report.json").write_text( + json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8" + ) + (out / "QUESTIONS.md").write_text( + _questions_md(questions, answers, report), encoding="utf-8" + ) report["questions_open"] = [q.key for q in questions_open] return report -def _questions_md(questions: Sequence[Question], answers: Mapping[str, Any], report: Mapping[str, Any]) -> str: +def _questions_md( + questions: Sequence[Question], answers: Mapping[str, Any], report: Mapping[str, Any] +) -> str: lines = ["# Questions for a person", ""] if not questions: - lines.append("None: every decision followed from the schema and the demonstrations.") + lines.append( + "None: every decision followed from the schema and the demonstrations." + ) for i, q in enumerate(questions, 1): done = _answer(answers, q.key) lines.append(f"{i}. {q.text}") lines.append(f" - Answer key: `{q.key}`. Proposed: `{q.proposal}`.") - lines.append(f" - {'Answered: `' + json.dumps(done, ensure_ascii=False) + '`' if done is not None else 'Not answered yet.'}") + lines.append( + f" - {'Answered: `' + json.dumps(done, ensure_ascii=False) + '`' if done is not None else 'Not answered yet.'}" + ) lines.append("") lines.append("## Kinds") lines.append("") for kind, rep in report["kinds"].items(): - lines.append(f"- `{kind}` ({rep['mode'] or 'no contract'}, main table `{rep['main_table']}`): {rep['emitted']} contract(s).") + lines.append( + f"- `{kind}` ({rep['mode'] or 'no contract'}, main table `{rep['main_table']}`): {rep['emitted']} contract(s)." + ) if rep.get("demonstrations") == 1 and rep["mode"] == "create": lines.append( " - One demonstration only: every column of a new record that the task " @@ -1927,7 +2148,11 @@ def _exists(contract: Any, params: Mapping[str, Any]) -> bool: def _now() -> str: - return _dt.datetime.now(_dt.timezone.utc).replace(tzinfo=None).isoformat(timespec="seconds") + return ( + _dt.datetime.now(_dt.timezone.utc) + .replace(tzinfo=None) + .isoformat(timespec="seconds") + ) def _parse_actor(items: Sequence[str]) -> dict[str, str]: @@ -1954,32 +2179,84 @@ def main(argv: Optional[Sequence[str]] = None) -> int: ), ) sub = ap.add_subparsers(dest="command", required=True) - s = sub.add_parser("schema", help="read the schema and check the account is read-only") - s.add_argument("--read", required=True, type=Path, help="read.yaml: sql_driver, sql_connect_args, sql_password_env") + s = sub.add_parser( + "schema", help="read the schema and check the account is read-only" + ) + s.add_argument( + "--read", + required=True, + type=Path, + help="read.yaml: sql_driver, sql_connect_args, sql_password_env", + ) s.add_argument("--out", type=Path, help="write schema.json here") - s = sub.add_parser("snapshot", help="snapshot every table before a demonstration write") + s = sub.add_parser( + "snapshot", help="snapshot every table before a demonstration write" + ) s.add_argument("--read", required=True, type=Path) - s.add_argument("--out", required=True, type=Path, help="e.g. work/before.json (holds row values: keep it private)") - s = sub.add_parser("record", help="snapshot again after the write and save the demonstration") + s.add_argument( + "--out", + required=True, + type=Path, + help="e.g. work/before.json (holds row values: keep it private)", + ) + s = sub.add_parser( + "record", help="snapshot again after the write and save the demonstration" + ) s.add_argument("--read", required=True, type=Path) s.add_argument("--before", required=True, type=Path) s.add_argument("--kind", required=True) - s.add_argument("--values", required=True, help="the task's values as JSON, or @file.json") - s.add_argument("--demos", required=True, type=Path, help="directory of demonstrations") - s.add_argument("--name", default="", help="file name stem (default: kind and a number)") + s.add_argument( + "--values", required=True, help="the task's values as JSON, or @file.json" + ) + s.add_argument( + "--demos", required=True, type=Path, help="directory of demonstrations" + ) + s.add_argument( + "--name", default="", help="file name stem (default: kind and a number)" + ) s = sub.add_parser("propose", help="analyze the demonstrations and write contracts") s.add_argument("--read", required=True, type=Path) - s.add_argument("--kinds", required=True, type=Path, help="kinds.yaml: kinds, parameters, examples") + s.add_argument( + "--kinds", + required=True, + type=Path, + help="kinds.yaml: kinds, parameters, examples", + ) s.add_argument("--demos", required=True, type=Path) s.add_argument("--out", required=True, type=Path) - s.add_argument("--answers", type=Path, help="answers.yaml with the person's answers") - s.add_argument("--actor-param", action="append", default=[], help="NAME=VALUE the caller passes for the acting user (e.g. bot_user_id=5); a column holding it is the actor") + s.add_argument( + "--answers", type=Path, help="answers.yaml with the person's answers" + ) + s.add_argument( + "--actor-param", + action="append", + default=[], + help="NAME=VALUE the caller passes for the acting user (e.g. bot_user_id=5); a column holding it is the actor", + ) s.add_argument("--environment-id", default="") - s.add_argument("--create-views", action="store_true", help="create a view for a table the SQL guard refuses by name (needs --admin-read)") - s.add_argument("--admin-read", type=Path, help="admin.yaml: a connection allowed to CREATE VIEW and GRANT") - s.add_argument("--reader-account", default="", help="the read account to GRANT SELECT on created views (MySQL: 'user'@'%%')") - s.add_argument("--grant-trigger", action="store_true", help="MySQL: GRANT TRIGGER to the read account through --admin-read, so the frame can see triggers") - s.add_argument("--no-validate", action="store_true", help="skip the dry read (tests only)") + s.add_argument( + "--create-views", + action="store_true", + help="create a view for a table the SQL guard refuses by name (needs --admin-read)", + ) + s.add_argument( + "--admin-read", + type=Path, + help="admin.yaml: a connection allowed to CREATE VIEW and GRANT", + ) + s.add_argument( + "--reader-account", + default="", + help="the read account to GRANT SELECT on created views (MySQL: 'user'@'%%')", + ) + s.add_argument( + "--grant-trigger", + action="store_true", + help="MySQL: GRANT TRIGGER to the read account through --admin-read, so the frame can see triggers", + ) + s.add_argument( + "--no-validate", action="store_true", help="skip the dry read (tests only)" + ) args = ap.parse_args(argv) try: read = load_read(args.read) @@ -1989,8 +2266,12 @@ def main(argv: Optional[Sequence[str]] = None) -> int: schema = store.read_schema(connect, dialect) problems = check_read_only(connect, dialect) if args.out: - args.out.write_text(json.dumps(schema.to_json(), indent=1), encoding="utf-8") - print(f"{dialect} {schema.database}: {len(schema.tables)} tables, {len(schema.views)} views") + args.out.write_text( + json.dumps(schema.to_json(), indent=1), encoding="utf-8" + ) + print( + f"{dialect} {schema.database}: {len(schema.tables)} tables, {len(schema.views)} views" + ) for p in problems: print(f"NOT READ-ONLY: {p}") return 1 if problems else 0 @@ -1998,7 +2279,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int: schema = store.read_schema(connect, dialect) rows = store.snapshot(connect, schema) store.save_snapshot(args.out, rows) - (args.out.with_suffix(".meta.json")).write_text(json.dumps({"taken_at": _now()}), encoding="utf-8") + (args.out.with_suffix(".meta.json")).write_text( + json.dumps({"taken_at": _now()}), encoding="utf-8" + ) print(f"snapshot of {len(rows)} tables -> {args.out}") return 0 if args.command == "record": @@ -2026,9 +2309,13 @@ def main(argv: Optional[Sequence[str]] = None) -> int: "diff": d.to_json(), } path = args.demos / f"{stem}.demo.json" - path.write_text(json.dumps(doc, indent=1, ensure_ascii=False), encoding="utf-8") + path.write_text( + json.dumps(doc, indent=1, ensure_ascii=False), encoding="utf-8" + ) os.chmod(path, 0o600) - print(f"{path}: {len(d.changes)} row change(s) in {', '.join(d.tables()) or 'no table'}") + print( + f"{path}: {len(d.changes)} row change(s) in {', '.join(d.tables()) or 'no table'}" + ) return 0 answers = load_yaml(args.answers) if args.answers else {} admin = load_read(args.admin_read) if args.admin_read else None @@ -2046,9 +2333,15 @@ def main(argv: Optional[Sequence[str]] = None) -> int: validate=not args.no_validate, grant_trigger_flag=args.grant_trigger, ) - print(f"questions: {len(report['questions'])} ({len(report['questions_open'])} open) -> {args.out / 'QUESTIONS.md'}") + print( + f"questions: {len(report['questions'])} ({len(report['questions_open'])} open) -> {args.out / 'QUESTIONS.md'}" + ) for kind, rep in report["kinds"].items(): - status = "; ".join(rep["refused"]) if rep["refused"] else f"{rep['emitted']} contract(s)" + status = ( + "; ".join(rep["refused"]) + if rep["refused"] + else f"{rep['emitted']} contract(s)" + ) print(f" {kind}: {status}") return 0 except (GenerateError, store.StoreError) as exc: diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index 1e5b566bc1..aab132a904 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -102,7 +102,9 @@ def __init__(self, port: int, db: str) -> None: self.port, self.db = port, db def con(self) -> Any: - c = psycopg2.connect(host="127.0.0.1", port=self.port, user="admin", dbname=self.db) + c = psycopg2.connect( + host="127.0.0.1", port=self.port, user="admin", dbname=self.db + ) c.autocommit = True return c @@ -118,10 +120,14 @@ def one(self, sql: str, args: tuple = ()) -> Any: def user(self, name: str) -> int: first, last = name.split(" ", 1) - return self.one("SELECT id FROM users WHERE firstname=%s AND lastname=%s", (first, last)) + return self.one( + "SELECT id FROM users WHERE firstname=%s AND lastname=%s", (first, last) + ) def create_issue(self, v: dict[str, Any], *, project_id: Any = None) -> int: - pid = project_id or self.one("SELECT id FROM projects WHERE name=%s", (v["project"],)) + pid = project_id or self.one( + "SELECT id FROM projects WHERE name=%s", (v["project"],) + ) sid = self.one("SELECT id FROM statuses WHERE name=%s", (v["status"],)) uid = self.user(v["assignee"]) if v.get("assignee") else None iid = self.one( @@ -142,7 +148,12 @@ def find(self, project: str, subject: str) -> int: def update_issue(self, iid: int, v: dict[str, Any], *, user: int = BOT) -> None: sets, details = [], [] if v.get("status") is not None: - sets.append(("status_id", self.one("SELECT id FROM statuses WHERE name=%s", (v["status"],)))) + sets.append( + ( + "status_id", + self.one("SELECT id FROM statuses WHERE name=%s", (v["status"],)), + ) + ) if v.get("assignee") is not None: sets.append(("assigned_to_id", self.user(v["assignee"]))) if v.get("due_date") is not None: @@ -153,7 +164,10 @@ def update_issue(self, iid: int, v: dict[str, Any], *, user: int = BOT) -> None: old = self.one(f"SELECT {col}::text FROM issues WHERE id=%s", (iid,)) self.q(f"UPDATE issues SET {col}=%s WHERE id=%s", (val, iid)) details.append((col, old, str(val))) - self.q("UPDATE issues SET updated_on=now(), lock_version=lock_version+1 WHERE id=%s", (iid,)) + self.q( + "UPDATE issues SET updated_on=now(), lock_version=lock_version+1 WHERE id=%s", + (iid,), + ) jid = self.one( "INSERT INTO journals (journalized_id, journalized_type, user_id, notes, created_on) " "VALUES (%s,'Issue',%s,%s,now()) RETURNING id", (iid, user, v.get("notes") or "")) # fmt: skip @@ -166,23 +180,71 @@ def update_issue(self, iid: int, v: dict[str, Any], *, user: int = BOT) -> None: "kinds": [ { "kind": "create_issue", - "parameters": ["project", "subject", "status", "assignee", "due_date", "customer_ref"], + "parameters": [ + "project", + "subject", + "status", + "assignee", + "due_date", + "customer_ref", + ], "examples": [ - {"values": {"project": "Billing Portal", "subject": "Tax rounding", "status": "New", - "assignee": "Jon Park", "due_date": "2026-11-20", "customer_ref": None}}, - {"values": {"project": "Help Desk", "subject": "Onboard Zoë", "status": "New", - "assignee": "Renée O'Connor", "due_date": None, "customer_ref": "007341"}}, + { + "values": { + "project": "Billing Portal", + "subject": "Tax rounding", + "status": "New", + "assignee": "Jon Park", + "due_date": "2026-11-20", + "customer_ref": None, + } + }, + { + "values": { + "project": "Help Desk", + "subject": "Onboard Zoë", + "status": "New", + "assignee": "Renée O'Connor", + "due_date": None, + "customer_ref": "007341", + } + }, ], }, { "kind": "update_issue", - "parameters": ["project", "subject", "status", "assignee", "due_date", "done_ratio", "notes"], + "parameters": [ + "project", + "subject", + "status", + "assignee", + "due_date", + "done_ratio", + "notes", + ], "examples": [ - {"values": {"project": "Help Desk", "subject": "Printer jams", "status": "In Progress", - "assignee": "Jon Park", "due_date": None, "done_ratio": None, "notes": None}}, - {"values": {"project": "Billing Portal", "subject": "Refund button missing", - "status": None, "assignee": None, "due_date": None, "done_ratio": 30, - "notes": "Customer confirmed the refund."}}, + { + "values": { + "project": "Help Desk", + "subject": "Printer jams", + "status": "In Progress", + "assignee": "Jon Park", + "due_date": None, + "done_ratio": None, + "notes": None, + } + }, + { + "values": { + "project": "Billing Portal", + "subject": "Refund button missing", + "status": None, + "assignee": None, + "due_date": None, + "done_ratio": 30, + "notes": "Customer confirmed the refund.", + } + }, ], }, ] @@ -192,7 +254,9 @@ def update_issue(self, iid: int, v: dict[str, Any], *, user: int = BOT) -> None: @pytest.fixture(scope="module") def generated(pg: Any) -> dict[str, Any]: root, port = pg["root"], pg["port"] - admin = psycopg2.connect(host="127.0.0.1", port=port, user="admin", dbname="postgres") + admin = psycopg2.connect( + host="127.0.0.1", port=port, user="admin", dbname="postgres" + ) admin.autocommit = True admin.cursor().execute("CREATE DATABASE tracker") admin.close() @@ -220,7 +284,11 @@ def demo(kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: demo("create_issue", ex["values"], lambda v=ex["values"]: app.create_issue(v)) for ex in KINDS["kinds"][1]["examples"]: v = ex["values"] - demo("update_issue", v, lambda v=v: app.update_issue(app.find(v["project"], v["subject"]), v)) + demo( + "update_issue", + v, + lambda v=v: app.update_issue(app.find(v["project"], v["subject"]), v), + ) argv = ["propose", *common, "--kinds", str(work / "kinds.yaml"), "--demos", str(work / "demos"), "--out", str(work / "out"), "--actor-param", f"bot_user_id={BOT}"] # fmt: skip assert gen.main(argv) == 0 @@ -234,16 +302,27 @@ def demo(kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: return {"app": app, "out": work / "out", "report": report, "read": read} -def _run(generated: dict[str, Any], kind: str, values: dict[str, Any], write: Callable[[], Any]) -> str: +def _run( + generated: dict[str, Any], + kind: str, + values: dict[str, Any], + write: Callable[[], Any], +) -> str: from openadapt_flow.verifier import Actor, begin_write, verify_write - contract, params = gen.choose(generated["out"] / "manifest.yaml", kind, values, {"bot_user_id": str(BOT)}) + contract, params = gen.choose( + generated["out"] / "manifest.yaml", kind, values, {"bot_user_id": str(BOT)} + ) with tempfile.TemporaryDirectory() as tmp: pending = begin_write(contract, params, actor=Actor("agent", "ops-bot"), ledger=None, write_key=f"t-{time.time_ns()}", workdir=tmp) # fmt: skip write() result = verify_write(pending) - return getattr(result.outcome, "value", str(result.outcome)) + " " + "; ".join(result.reasons)[:400] + return ( + getattr(result.outcome, "value", str(result.outcome)) + + " " + + "; ".join(result.reasons)[:400] + ) def test_questions_and_contracts(generated: dict[str, Any]) -> None: @@ -257,7 +336,9 @@ def test_questions_and_contracts(generated: dict[str, Any]) -> None: # Four change fields the examples give (status, assignee, done ratio, # notes): every non-empty subset gets a contract. No example gives a due # date, so no contract changes one. - assert kinds["update_issue"]["emitted"] == 15, json.dumps(kinds["update_issue"], indent=1) + assert kinds["update_issue"]["emitted"] == 15, json.dumps( + kinds["update_issue"], indent=1 + ) assert "journal_details" in kinds["update_issue"]["logs"] @@ -265,7 +346,9 @@ def test_a_correct_create_is_confirmed(generated: dict[str, Any]) -> None: app = generated["app"] v = {"project": "Billing Portal", "subject": "Dark mode", "status": "New", "assignee": "Renée O'Connor", "due_date": "2026-12-01", "customer_ref": "0099"} # fmt: skip - assert _run(generated, "create_issue", v, lambda: app.create_issue(v)).startswith("CONFIRMED") + assert _run(generated, "create_issue", v, lambda: app.create_issue(v)).startswith( + "CONFIRMED" + ) def test_found_existing_with_no_write_is_confirmed(generated: dict[str, Any]) -> None: @@ -276,12 +359,16 @@ def test_found_existing_with_no_write_is_confirmed(generated: dict[str, Any]) -> assert _run(generated, "create_issue", v, lambda: None).startswith("CONFIRMED") -def test_an_undemonstrated_update_combination_is_confirmed(generated: dict[str, Any]) -> None: +def test_an_undemonstrated_update_combination_is_confirmed( + generated: dict[str, Any], +) -> None: app = generated["app"] v = {"project": "Help Desk", "subject": "VPN drops", "status": "Resolved", "assignee": None, "due_date": None, "done_ratio": 80, "notes": "Fixed the tunnel."} # fmt: skip iid = app.find("Help Desk", "VPN drops") - assert _run(generated, "update_issue", v, lambda: app.update_issue(iid, v)).startswith("CONFIRMED") + assert _run( + generated, "update_issue", v, lambda: app.update_issue(iid, v) + ).startswith("CONFIRMED") CREATE = {"project": "Billing Portal", "subject": "Export CSV", "status": "New", @@ -332,4 +419,6 @@ def test_each_fault_is_stopped(generated: dict[str, Any], name: str) -> None: for t, rows in snapshot.items(): cur.execute(f"DELETE FROM {t}") for r in rows: - cur.execute(f"INSERT INTO {t} VALUES ({', '.join(['%s'] * len(r))})", r) + cur.execute( + f"INSERT INTO {t} VALUES ({', '.join(['%s'] * len(r))})", r + ) From 1532b24e90b40eba3f423a3b8492ea808f053cc9 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:05:39 -0400 Subject: [PATCH 09/94] WIP: found flavor for updates already in place --- packages/flow/docs/GENERATE_CONTRACTS.md | 17 ++++++++++++----- .../openadapt_flow/generate/verify_contract.py | 8 ++++---- .../flow/tests/test_generate_verify_contract.py | 17 ++++++++++++++++- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 53aab255ce..821f0597cb 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -83,8 +83,8 @@ server, use `openadapt_flow.generate.ensure_contract` (an ## What it writes - `contracts//..yaml`: one contract per field set. The - flavor is `create` and `found` for a kind that adds a record, `update` for a - kind that changes one. + flavor is `create` or `update`, and each has a `found` twin for a task + whose record already holds its values. - `manifest.yaml`: which contract fits which task values. - `QUESTIONS.md`: the numbered questions, the proposed answers, and per kind how many contracts were written and why any were not. @@ -149,9 +149,10 @@ gets a contract per length, from 1 to 10 items. - `create`: exactly one new main record holds the task's values, and everything else in the new row is what the demonstrations showed, or is bookkeeping. -- `found`: the record already exists, and nothing in the database changed. - `choose` reads the store first and picks `found` when a record with the - task's values is already there. +- `found`: the record already holds the task's values, and nothing in the + database changed. `choose` reads the store first and picks `found` when a + record with the task's values is already there: a "make sure it exists" + task whose record exists, or an update that is already in place. - `update`: exactly one record matches the task's identity values and holds the new values. Only the fields the task changes may change. @@ -207,6 +208,12 @@ why. - Columns of a new record that differ between demonstrations and aren't task values (an invoice's computed totals, its draft reference) are not checked. The bookkeeping question lists them. +- Free-text columns of a new record that the task doesn't give, and that + every demonstration left empty (a line description, a note), are not + checked: an agent may fill them from the record it copies. +- With one demonstration of a create, every other column must hold the value + it held then, including one computed from the task's values (a price with + tax). `QUESTIONS.md` says so; record a second create with other values. - On PostgreSQL there is no whole-database frame and no recheck: tables proposed as logs are not read, and a write that lands after the check is not seen. diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 0342476c1e..58632ec7ab 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1963,7 +1963,7 @@ def propose( manifest["kinds"][p.kind] = entry report["kinds"][p.kind] = rep continue - flavors = ["create", "found"] if p.mode == "create" else ["update"] + flavors = ["create", "found"] if p.mode == "create" else ["update", "found"] for v in variants(schema, p, spec, answers): if any( f not in p.locations @@ -2115,12 +2115,12 @@ def choose( continue params = {**flatten(values), **dict(extra_params or {})} base = manifest_path.parent - if entry["mode"] == "update": - return Contract.load(base / var["update"]), params + # The record already holds the task's values (found, or an update + # already in place): nothing may change. Else the create or update. found = Contract.load(base / var["found"]) if _exists(found, params): return found, params - return Contract.load(base / var["create"]), params + return Contract.load(base / var[entry["mode"]]), params raise GenerateError( f"no {kind} contract for the fields {sorted(given)}" + (f" and lists {lists}" if lists else "") diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index aab132a904..d3cf81c4b4 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -371,7 +371,22 @@ def test_an_undemonstrated_update_combination_is_confirmed( ).startswith("CONFIRMED") -CREATE = {"project": "Billing Portal", "subject": "Export CSV", "status": "New", +def test_an_update_already_in_place_with_no_write_is_confirmed( + generated: dict[str, Any], +) -> None: + app = generated["app"] + v = {"project": "Help Desk", "subject": "VPN drops", "status": None, "assignee": None, + "due_date": None, "done_ratio": 80, "notes": None} # fmt: skip + app.q("UPDATE issues SET done_ratio=80 WHERE id=%s", (app.find("Help Desk", "VPN drops"),)) + assert _run(generated, "update_issue", v, lambda: None).startswith("CONFIRMED") + # The same task with a stray write on the issue is stopped. + stray = _run(generated, "update_issue", v, + lambda: app.q("UPDATE issues SET is_private=true WHERE subject='VPN drops'")) # fmt: skip + app.q("UPDATE issues SET is_private=false WHERE subject='VPN drops'") + assert not stray.startswith("CONFIRMED"), stray + + +CREATE ={"project": "Billing Portal", "subject": "Export CSV", "status": "New", "assignee": "Jon Park", "due_date": "2026-12-05", "customer_ref": "0042"} # fmt: skip UPDATE = {"project": "Billing Portal", "subject": "Refund button missing", "status": "Resolved", "assignee": None, "due_date": None, "done_ratio": None, "notes": "Done."} # fmt: skip From da9d887f389ad25d3d50e4c68633f769e8c6cf59 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:06:31 -0400 Subject: [PATCH 10/94] WIP: validation runs the declared recheck on late-write faults --- .../flow/benchmark/generate_contracts/summarize.py | 3 ++- .../flow/benchmark/generate_contracts/validate.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/flow/benchmark/generate_contracts/summarize.py b/packages/flow/benchmark/generate_contracts/summarize.py index efe88d438c..a277508deb 100644 --- a/packages/flow/benchmark/generate_contracts/summarize.py +++ b/packages/flow/benchmark/generate_contracts/summarize.py @@ -41,7 +41,8 @@ def main() -> None: misses, false_stops = [], [] for r in rows: k = r["kind"] - ok = r["verdict"]["outcome"] == "CONFIRMED" + # A recheck that refutes a confirmed write turns it into a stop (late). + ok = r["verdict"]["outcome"] == "CONFIRMED" and r.get("recheck", {}).get("outcome", "CONFIRMED") == "CONFIRMED" label = str(r["oracle"].get("label")) harm = label not in GOOD sc = scope.get(r["case"].split("-", 1)[1], "in") diff --git a/packages/flow/benchmark/generate_contracts/validate.py b/packages/flow/benchmark/generate_contracts/validate.py index d7371955bb..6e2b4f631d 100644 --- a/packages/flow/benchmark/generate_contracts/validate.py +++ b/packages/flow/benchmark/generate_contracts/validate.py @@ -26,7 +26,7 @@ from demo_lib import FCG, fixture, load from openadapt_flow.generate import verify_contract as gen -from openadapt_flow.verifier import Actor, begin_write, verify_write +from openadapt_flow.verifier import Actor, begin_write, recheck, verify_write LATE_WAIT_S = 150 @@ -128,6 +128,18 @@ def main() -> int: time.sleep(late) if hasattr(fx, "wait_late_writes"): fx.wait_late_writes() + if rec["verdict"]["outcome"] == "CONFIRMED" and rec["verdict"].get("recheck_due_at"): + # A write that lands after the check: the recheck the contract + # declares (frame.database.recheck_after_s) reads it. + try: + late_r = recheck(contract, r, params=params, + actor=Actor("agent", "generated-check"), wait=True) # fmt: skip + rec["recheck"] = { + "outcome": getattr(late_r.outcome, "value", str(late_r.outcome)), + "reasons": list(late_r.reasons), + } + except Exception as exc: # noqa: BLE001 + rec["recheck"] = {"outcome": "RECHECK_CRASH", "reasons": [f"{type(exc).__name__}: {exc}"]} after = oracle.snapshot() try: rec["oracle"] = oracle.judge(task, before, after) From a7696ab618330b27b0a883607b660b2415b00d1f Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:28:48 -0400 Subject: [PATCH 11/94] WIP: side rows in the bookkeeping question; Dolibarr results --- CAPABILITIES.md | 1 + .../results/dolibarr/QUESTIONS.md | 20 + .../results/dolibarr/answers.yaml | 3 + ...l+name+phone+town+zip.8386ba67.create.yaml | 352 +++++++++++++ ...il+name+phone+town+zip.8386ba67.found.yaml | 106 ++++ ...ice_date+town+linesx2.17de9df9.create.yaml | 496 ++++++++++++++++++ ...oice_date+town+linesx2.17de9df9.found.yaml | 93 ++++ ...customer+town+linesx2.c91f9518.create.yaml | 484 +++++++++++++++++ .../customer+town+linesx2.c91f9518.found.yaml | 88 ++++ ...excl_vat+ref+vat_rate.bcd72fd0.create.yaml | 426 +++++++++++++++ ..._excl_vat+ref+vat_rate.bcd72fd0.found.yaml | 104 ++++ ...w_email+new_phone+town.be743cce.found.yaml | 101 ++++ ..._email+new_phone+town.be743cce.update.yaml | 218 ++++++++ ...w_email+new_phone+town.110f7554.found.yaml | 99 ++++ ..._email+new_phone+town.110f7554.update.yaml | 215 ++++++++ .../results/dolibarr/manifest.yaml | 311 +++++++++++ .../results/dolibarr/report.json | 157 ++++++ .../generate/verify_contract.py | 15 +- 18 files changed, 3288 insertions(+), 1 deletion(-) create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/report.json diff --git a/CAPABILITIES.md b/CAPABILITIES.md index 7bf1771e1e..07d3553c05 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,6 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, and a recheck window the person confirms. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies: Dolibarr 3 questions; RESULTS_PLACEHOLDER) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md new file mode 100644 index 0000000000..92a0749f46 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md @@ -0,0 +1,20 @@ +# Questions for a person + +1. Is this the app's own bookkeeping, which a write may change without a task asking? Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. + - Answer key: `bookkeeping`. Proposed: `accept`. + - Answered: `"accept"` + +2. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. + - Answer key: `recheck_after_s`. Proposed: `300`. + - Answered: `300` + +3. Kind `create_draft_invoice`: `invoice_date` is in several places: [0] llx_facture.datef (date); [1] llx_facture.date_lim_reglement (date) Answer `locations.create_draft_invoice: {: }` to pick one. + - Answer key: `locations.create_draft_invoice`. Proposed: `None`. + - Answered: `{"invoice_date": 0}` + +## Kinds + +- `create_customer` (create, main table `llx_societe`): 1 contract(s). +- `create_product` (create, main table `llx_product`): 1 contract(s). +- `create_draft_invoice` (create, main table `llx_facture`): 10 contract(s). +- `update_customer` (update, main table `llx_societe`): 15 contract(s). diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml new file mode 100644 index 0000000000..d81a017c37 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml @@ -0,0 +1,3 @@ +bookkeeping: accept +recheck_after_s: 300 +locations.create_draft_invoice: {invoice_date: 0} diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml new file mode 100644 index 0000000000..095d9bcd5b --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml @@ -0,0 +1,352 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_customer, create, fields address, country, email, name, phone, town, zip. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key`, (SELECT CAST(a1.`label` + AS CHAR) FROM `llx_c_country` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_pays` AS BINARY)) + AS `p__country`, CASE WHEN t.`capital` IS NULL THEN '1' ELSE '0' END AS `pin__capital`, CASE WHEN + t.`client` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__client`, CASE WHEN t.`cond_reglement` + IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement`, CASE WHEN t.`cond_reglement_supplier` + IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement_supplier`, CASE WHEN t.`customer_bad` = + CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_bad`, CASE WHEN t.`customer_rate` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_rate`, CASE WHEN t.`entity` + = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fk_account` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_barcode_type` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, CASE WHEN t.`fk_departement` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_departement`, CASE WHEN t.`fk_effectif` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_effectif`, CASE WHEN t.`fk_forme_juridique` IS NULL THEN '1' ELSE + '0' END AS `pin__fk_forme_juridique`, CASE WHEN t.`fk_incoterms` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_incoterms`, CASE WHEN t.`fk_multicurrency` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_shipping_method` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_shipping_method`, CASE WHEN t.`fk_stcomm` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_stcomm`, CASE WHEN t.`fk_typent` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_typent`, CASE WHEN t.`fk_warehouse` IS NULL THEN '1' ELSE '0' + END AS `pin__fk_warehouse`, CASE WHEN t.`fournisseur` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE + '0' END AS `pin__fournisseur`, CASE WHEN t.`geolat` IS NULL THEN '1' ELSE '0' END AS `pin__geolat`, + CASE WHEN t.`geolong` IS NULL THEN '1' ELSE '0' END AS `pin__geolong`, CASE WHEN t.`geopoint` + IS NULL THEN '1' ELSE '0' END AS `pin__geopoint`, CASE WHEN t.`localtax1_assuj` = CAST('0' AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_assuj`, CASE WHEN t.`localtax1_value` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_value`, CASE WHEN t.`localtax2_assuj` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_assuj`, CASE WHEN t.`localtax2_value` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_value`, CASE WHEN t.`mode_reglement` + IS NULL THEN '1' ELSE '0' END AS `pin__mode_reglement`, CASE WHEN t.`mode_reglement_supplier` + IS NULL THEN '1' ELSE '0' END AS `pin__mode_reglement_supplier`, CASE WHEN t.`order_min_amount` + IS NULL THEN '1' ELSE '0' END AS `pin__order_min_amount`, CASE WHEN t.`outstanding_limit` IS NULL + THEN '1' ELSE '0' END AS `pin__outstanding_limit`, CASE WHEN t.`parent` IS NULL THEN '1' ELSE + '0' END AS `pin__parent`, CASE WHEN t.`price_level` IS NULL THEN '1' ELSE '0' END AS `pin__price_level`, + CASE WHEN t.`remise_client` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_client`, + CASE WHEN t.`remise_supplier` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_supplier`, + CASE WHEN t.`status` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__status`, CASE + WHEN t.`statut` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__statut`, CASE WHEN + t.`supplier_order_min_amount` IS NULL THEN '1' ELSE '0' END AS `pin__supplier_order_min_amount`, + CASE WHEN t.`supplier_rate` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__supplier_rate`, + CASE WHEN t.`transport_mode` IS NULL THEN '1' ELSE '0' END AS `pin__transport_mode`, CASE WHEN + t.`transport_mode_supplier` IS NULL THEN '1' ELSE '0' END AS `pin__transport_mode_supplier`, CASE + WHEN t.`tva_assuj` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tva_assuj`, CASE + WHEN t.`vat_reverse_charge` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__vat_reverse_charge` + FROM `llx_societe` t + raw_llx_actioncomm: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` + AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, + CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` + AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` + AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS + `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, + CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, + CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` + AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) + AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, + CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` + AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` + AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` + AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS + CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) + AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, + CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` + AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS + `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` + AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` + AS CHAR) AS `extraparams` FROM `llx_actioncomm` t + raw_llx_actioncomm_resources: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` + AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` + AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` + AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + address: + param: address + p__country: + param: country + email: + param: email + nom: + param: name + phone: + param: phone + kind: phone + town: + param: town + zip: + param: zip + pin__capital: + literal: '1' + pin__client: + literal: '1' + pin__cond_reglement: + literal: '1' + pin__cond_reglement_supplier: + literal: '1' + pin__customer_bad: + literal: '1' + pin__customer_rate: + literal: '1' + pin__entity: + literal: '1' + pin__fk_account: + literal: '1' + pin__fk_barcode_type: + literal: '1' + pin__fk_departement: + literal: '1' + pin__fk_effectif: + literal: '1' + pin__fk_forme_juridique: + literal: '1' + pin__fk_incoterms: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_shipping_method: + literal: '1' + pin__fk_stcomm: + literal: '1' + pin__fk_typent: + literal: '1' + fk_user_creat: + param: bot_user_id + fk_user_modif: + param: bot_user_id + pin__fk_warehouse: + literal: '1' + pin__fournisseur: + literal: '1' + pin__geolat: + literal: '1' + pin__geolong: + literal: '1' + pin__geopoint: + literal: '1' + pin__localtax1_assuj: + literal: '1' + pin__localtax1_value: + literal: '1' + pin__localtax2_assuj: + literal: '1' + pin__localtax2_value: + literal: '1' + pin__mode_reglement: + literal: '1' + pin__mode_reglement_supplier: + literal: '1' + pin__order_min_amount: + literal: '1' + pin__outstanding_limit: + literal: '1' + pin__parent: + literal: '1' + pin__price_level: + literal: '1' + pin__remise_client: + literal: '1' + pin__remise_supplier: + literal: '1' + socialnetworks: + literal: 'null' + pin__status: + literal: '1' + pin__statut: + literal: '1' + pin__supplier_order_min_amount: + literal: '1' + pin__supplier_rate: + literal: '1' + pin__transport_mode: + literal: '1' + pin__transport_mode_supplier: + literal: '1' + pin__tva_assuj: + literal: '1' + pin__vat_reverse_charge: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm + match: + code: + literal: AC_COMPANY_CREATE + entity: + literal: '1' + event_paid: + literal: '0' + fk_action: + literal: '40' + fk_parent: + literal: '0' + fk_user_action: + param: bot_user_id + fk_user_author: + param: bot_user_id + fulldayevent: + literal: '0' + percent: + literal: '-1' + priority: + literal: '0' + status: + literal: '0' + transparency: + literal: '0' + visibility: + literal: default + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources + match: + element_type: + literal: user + fk_element: + param: bot_user_id + mandatory: + literal: '0' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: + llx_actioncomm: raw_llx_actioncomm + llx_actioncomm_resources: raw_llx_actioncomm_resources + settle_s: 1 + settle_attempts: 5 + new_row_fields: + llx_actioncomm: + - datec + - datep + - datep2 + - durationp + - elementtype + - email_from + - email_msgid + - email_sender + - email_subject + - email_to + - email_tobcc + - email_tocc + - errors_to + - extraparams + - fk_soc + - id + - import_key + - ip + - label + - location + - note + - recurid + - recurrule + - ref + - ref_ext + - reply_to + - tms + llx_actioncomm_resources: + - answer_status + - fk_actioncomm + - rowid + - transparency + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml new file mode 100644 index 0000000000..c54cbd55ef --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml @@ -0,0 +1,106 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_customer, found, fields address, country, email, name, phone, town, zip. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key`, (SELECT CAST(a1.`label` + AS CHAR) FROM `llx_c_country` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_pays` AS BINARY)) + AS `p__country` FROM `llx_societe` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + address: + param: address + p__country: + param: country + email: + param: email + nom: + param: name + phone: + param: phone + kind: phone + town: + param: town + zip: + param: zip + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml new file mode 100644 index 0000000000..46aa29ec26 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml @@ -0,0 +1,496 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, create, fields customer, invoice_date, town, linesx2. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` + AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, + CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` + AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, + CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` + AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) + AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` + AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) + AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS + `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, + CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, + CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, + CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, + CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, + CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` + AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` + AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, + CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS + `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS + `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) + AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` + AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` + AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, + CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS + `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, + CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, + CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, + CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) + AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, + CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, CASE WHEN t.`datef` + = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, (SELECT CAST(a1.`town` AS CHAR) + FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town`, CASE WHEN t.`close_missing_amount` + IS NULL THEN '1' ELSE '0' END AS `pin__close_missing_amount`, CASE WHEN t.`date_closing` IS NULL + THEN '1' ELSE '0' END AS `pin__date_closing`, CASE WHEN t.`date_pointoftax` IS NULL THEN '1' ELSE + '0' END AS `pin__date_pointoftax`, CASE WHEN t.`date_valid` IS NULL THEN '1' ELSE '0' END AS `pin__date_valid`, + CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE + WHEN t.`fk_account` IS NULL THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_cond_reglement` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_cond_reglement`, CASE WHEN t.`fk_fac_rec_source` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_fac_rec_source`, CASE WHEN t.`fk_facture_source` IS + NULL THEN '1' ELSE '0' END AS `pin__fk_facture_source`, CASE WHEN t.`fk_incoterms` = (SELECT s.`fk_incoterms` + FROM `llx_societe` s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_incoterms`, + CASE WHEN t.`fk_mode_reglement` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_mode_reglement`, + CASE WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_societe` s WHERE s.`rowid` + = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_projet` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_projet`, CASE WHEN t.`fk_statut` = (SELECT s.`statut` FROM `llx_societe` + s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_statut`, CASE WHEN t.`fk_transport_mode` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_transport_mode`, CASE WHEN t.`fk_user_closing` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_user_closing`, CASE WHEN t.`fk_user_modif` IS NULL THEN '1' + ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_user_valid` IS NULL THEN '1' ELSE '0' END + AS `pin__fk_user_valid`, CASE WHEN t.`is_also_delivery_note` = CAST('0' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__is_also_delivery_note`, CASE WHEN t.`localtax1` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax1`, CASE WHEN t.`localtax2` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax2`, CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, CASE WHEN t.`paye` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__paye`, CASE WHEN t.`prorata_discount` IS NULL THEN '1' ELSE '0' + END AS `pin__prorata_discount`, CASE WHEN t.`remise` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE + '0' END AS `pin__remise`, CASE WHEN t.`remise_absolue` = CAST('0' AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `pin__remise_absolue`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`retained_warranty` = CAST('0' AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty`, CASE WHEN t.`retained_warranty_date_limit` + IS NULL THEN '1' ELSE '0' END AS `pin__retained_warranty_date_limit`, CASE WHEN t.`retained_warranty_fk_cond_reglement` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty_fk_cond_reglement`, + CASE WHEN t.`revenuestamp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__revenuestamp`, + CASE WHEN t.`situation_counter` IS NULL THEN '1' ELSE '0' END AS `pin__situation_counter`, CASE + WHEN t.`situation_cycle_ref` IS NULL THEN '1' ELSE '0' END AS `pin__situation_cycle_ref`, CASE + WHEN t.`situation_final` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_final`, + CASE WHEN t.`subtype` IS NULL THEN '1' ELSE '0' END AS `pin__subtype`, CASE WHEN t.`type` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__type` FROM `llx_facture` t + sql_query_params: + v0: &id002 + param: invoice_date + rows_llx_facturedet: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_facture` + AS CHAR) AS `fk_facture`, CAST(t.`fk_parent_line` AS CHAR) AS `fk_parent_line`, CAST(t.`fk_product` + AS CHAR) AS `fk_product`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS + `description`, CAST(t.`vat_src_code` AS CHAR) AS `vat_src_code`, CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, + CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, + CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, + CAST(t.`qty` AS CHAR) AS `qty`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise` + AS CHAR) AS `remise`, CAST(t.`fk_remise_except` AS CHAR) AS `fk_remise_except`, CAST(t.`subprice` + AS CHAR) AS `subprice`, CAST(t.`subprice_ttc` AS CHAR) AS `subprice_ttc`, CAST(t.`price` AS CHAR) + AS `price`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, + CAST(t.`total_localtax1` AS CHAR) AS `total_localtax1`, CAST(t.`total_localtax2` AS CHAR) AS `total_localtax2`, + CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, CAST(t.`product_type` AS CHAR) AS `product_type`, + CAST(t.`date_start` AS CHAR) AS `date_start`, CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`info_bits` + AS CHAR) AS `info_bits`, CAST(t.`buy_price_ht` AS CHAR) AS `buy_price_ht`, CAST(t.`fk_product_fournisseur_price` + AS CHAR) AS `fk_product_fournisseur_price`, CAST(t.`batch` AS CHAR) AS `batch`, CAST(t.`fk_warehouse` + AS CHAR) AS `fk_warehouse`, CAST(t.`special_code` AS CHAR) AS `special_code`, CAST(t.`rang` AS + CHAR) AS `rang`, CAST(t.`fk_contract_line` AS CHAR) AS `fk_contract_line`, CAST(t.`fk_unit` AS + CHAR) AS `fk_unit`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_code_ventilation` + AS CHAR) AS `fk_code_ventilation`, CAST(t.`situation_percent` AS CHAR) AS `situation_percent`, + CAST(t.`fk_prev_id` AS CHAR) AS `fk_prev_id`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, + CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_subprice` + AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, + CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` + AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CAST(a2.`nom` AS CHAR) FROM `llx_societe` + a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS + `main__customer`, CASE WHEN (SELECT a1.`datef` FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) + = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `main__invoice_date`, (SELECT (SELECT CAST(a2.`town` + AS CHAR) FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` + = t.`fk_facture`) AS `main__town`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE + CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_0_product_ref`, CASE + WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v1)s AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `p__lines_0_qty`, CASE WHEN t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE WHEN t.`date_end` IS NULL THEN '1' ELSE '0' + END AS `pin__date_end`, CASE WHEN t.`date_start` IS NULL THEN '1' ELSE '0' END AS `pin__date_start`, + CASE WHEN t.`fk_code_ventilation` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, + CASE WHEN t.`fk_contract_line` IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE + WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` + = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_prev_id`, CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, CASE WHEN t.`fk_remise_except` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE + '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` FROM `llx_facture` + s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, CASE WHEN + t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_user_modif`, + CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_warehouse`, + CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__info_bits`, + CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_tx`, + CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_tx`, + CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` FROM `llx_facture` s WHERE s.`rowid` + = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, CASE WHEN t.`multicurrency_subprice` + = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END + AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE WHEN t.`price` IS NULL THEN '1' + ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT s.`type` FROM `llx_facture` + s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, CASE WHEN t.`rang` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN t.`remise` = (SELECT + s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__remise`, + CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_percent`, + CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_percent`, + CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__special_code`, + CASE WHEN t.`subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__subprice`, CASE WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__subprice_ttc`, CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax1`, + CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) + THEN '1' ELSE '0' END AS `pin__total_localtax2`, CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM + `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx`, (SELECT + CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` + AS BINARY)) AS `p__lines_1_product_ref`, CASE WHEN CAST(%(v2)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_1_qty`, CASE WHEN + t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE + WHEN t.`date_end` IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS + NULL THEN '1' ELSE '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_multicurrency` = (SELECT + s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' + END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE '0' END AS + `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, + CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, + CASE WHEN t.`fk_remise_except` IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE + WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` + = (SELECT s.`fk_user_author` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE + '0' END AS `pin__fk_user_author`, CASE WHEN t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__info_bits`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, + CASE WHEN t.`multicurrency_subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` + = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE + WHEN t.`price` IS NULL THEN '1' ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT + s.`type` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, + CASE WHEN t.`rang` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN + t.`remise` = (SELECT s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' + ELSE '0' END AS `pin__remise`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__situation_percent`, CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__special_code`, CASE WHEN t.`subprice` = (SELECT s.`price` FROM + `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__subprice`, CASE + WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__subprice_ttc`, + CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) + THEN '1' ELSE '0' END AS `pin__total_localtax1`, CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax2`, + CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__tva_tx` FROM `llx_facturedet` t + sql_query_params: + v0: *id002 + v1: + param: lines.0.qty + v2: + param: lines.1.qty +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__customer: + param: customer + p__invoice_date: + literal: '1' + p__town: + param: town + pin__close_missing_amount: + literal: '1' + pin__date_closing: + literal: '1' + pin__date_pointoftax: + literal: '1' + pin__date_valid: + literal: '1' + pin__entity: + literal: '1' + pin__fk_account: + literal: '1' + pin__fk_cond_reglement: + literal: '1' + pin__fk_fac_rec_source: + literal: '1' + pin__fk_facture_source: + literal: '1' + pin__fk_incoterms: + literal: '1' + pin__fk_mode_reglement: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_projet: + literal: '1' + pin__fk_statut: + literal: '1' + pin__fk_transport_mode: + literal: '1' + fk_user_author: + param: bot_user_id + pin__fk_user_closing: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_user_valid: + literal: '1' + pin__is_also_delivery_note: + literal: '1' + pin__localtax1: + literal: '1' + pin__localtax2: + literal: '1' + multicurrency_code: + literal: EUR + pin__multicurrency_tx: + literal: '1' + pin__paye: + literal: '1' + pin__prorata_discount: + literal: '1' + pin__remise: + literal: '1' + pin__remise_absolue: + literal: '1' + pin__remise_percent: + literal: '1' + pin__retained_warranty: + literal: '1' + pin__retained_warranty_date_limit: + literal: '1' + pin__retained_warranty_fk_cond_reglement: + literal: '1' + pin__revenuestamp: + literal: '1' + pin__situation_counter: + literal: '1' + pin__situation_cycle_ref: + literal: '1' + pin__situation_final: + literal: '1' + pin__subtype: + literal: '1' + pin__type: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__invoice_date: + literal: '1' + main__town: + param: town + p__lines_0_product_ref: + param: lines.0.product_ref + p__lines_0_qty: + literal: '1' + pin__buy_price_ht: + literal: '1' + pin__date_end: + literal: '1' + pin__date_start: + literal: '1' + pin__fk_code_ventilation: + literal: '1' + pin__fk_contract_line: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_parent_line: + literal: '1' + pin__fk_prev_id: + literal: '1' + pin__fk_product_fournisseur_price: + literal: '1' + pin__fk_remise_except: + literal: '1' + pin__fk_unit: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_warehouse: + literal: '1' + pin__info_bits: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__multicurrency_code: + literal: '1' + pin__multicurrency_subprice: + literal: '1' + pin__multicurrency_subprice_ttc: + literal: '1' + pin__price: + literal: '1' + pin__product_type: + literal: '1' + pin__rang: + literal: '1' + pin__remise: + literal: '1' + pin__remise_percent: + literal: '1' + pin__situation_percent: + literal: '1' + pin__special_code: + literal: '1' + pin__subprice: + literal: '1' + pin__subprice_ttc: + literal: '1' + pin__total_localtax1: + literal: '1' + pin__total_localtax2: + literal: '1' + pin__tva_tx: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__invoice_date: + literal: '1' + main__town: + param: town + p__lines_1_product_ref: + param: lines.1.product_ref + p__lines_1_qty: + literal: '1' + pin__buy_price_ht: + literal: '1' + pin__date_end: + literal: '1' + pin__date_start: + literal: '1' + pin__fk_code_ventilation: + literal: '1' + pin__fk_contract_line: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_parent_line: + literal: '1' + pin__fk_prev_id: + literal: '1' + pin__fk_product_fournisseur_price: + literal: '1' + pin__fk_remise_except: + literal: '1' + pin__fk_unit: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_warehouse: + literal: '1' + pin__info_bits: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__multicurrency_code: + literal: '1' + pin__multicurrency_subprice: + literal: '1' + pin__multicurrency_subprice_ttc: + literal: '1' + pin__price: + literal: '1' + pin__product_type: + literal: '1' + pin__rang: + literal: '1' + pin__remise: + literal: '1' + pin__remise_percent: + literal: '1' + pin__situation_percent: + literal: '1' + pin__special_code: + literal: '1' + pin__subprice: + literal: '1' + pin__subprice_ttc: + literal: '1' + pin__total_localtax1: + literal: '1' + pin__total_localtax2: + literal: '1' + pin__tva_tx: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_facture + mode: skip + reason: read in full, every column, by this contract's own surface + - table: llx_facturedet + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml new file mode 100644 index 0000000000..e9e6d0f41e --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml @@ -0,0 +1,93 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, found, fields customer, invoice_date, town, linesx2. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` + AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, + CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` + AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, + CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` + AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) + AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` + AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) + AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS + `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, + CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, + CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, + CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, + CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, + CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` + AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` + AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, + CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS + `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS + `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) + AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` + AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` + AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, + CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS + `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, + CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, + CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, + CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) + AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, + CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, CASE WHEN t.`datef` + = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, (SELECT CAST(a1.`town` AS CHAR) + FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town` FROM `llx_facture` t + sql_query_params: + v0: + param: invoice_date +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__customer: + param: customer + p__invoice_date: + literal: '1' + p__town: + param: town + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_facture + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml new file mode 100644 index 0000000000..18d772fe81 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml @@ -0,0 +1,484 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, create, fields customer, town, linesx2. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` + AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, + CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` + AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, + CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` + AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) + AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` + AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) + AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS + `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, + CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, + CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, + CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, + CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, + CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` + AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` + AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, + CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS + `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS + `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) + AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` + AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` + AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, + CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS + `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, + CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, + CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, + CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) + AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, + CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, (SELECT CAST(a1.`town` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town`, CASE WHEN t.`close_missing_amount` + IS NULL THEN '1' ELSE '0' END AS `pin__close_missing_amount`, CASE WHEN t.`date_closing` IS NULL + THEN '1' ELSE '0' END AS `pin__date_closing`, CASE WHEN t.`date_pointoftax` IS NULL THEN '1' ELSE + '0' END AS `pin__date_pointoftax`, CASE WHEN t.`date_valid` IS NULL THEN '1' ELSE '0' END AS `pin__date_valid`, + CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE + WHEN t.`fk_account` IS NULL THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_cond_reglement` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_cond_reglement`, CASE WHEN t.`fk_fac_rec_source` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_fac_rec_source`, CASE WHEN t.`fk_facture_source` IS + NULL THEN '1' ELSE '0' END AS `pin__fk_facture_source`, CASE WHEN t.`fk_incoterms` = (SELECT s.`fk_incoterms` + FROM `llx_societe` s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_incoterms`, + CASE WHEN t.`fk_mode_reglement` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_mode_reglement`, + CASE WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_societe` s WHERE s.`rowid` + = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_projet` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_projet`, CASE WHEN t.`fk_statut` = (SELECT s.`statut` FROM `llx_societe` + s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_statut`, CASE WHEN t.`fk_transport_mode` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_transport_mode`, CASE WHEN t.`fk_user_closing` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_user_closing`, CASE WHEN t.`fk_user_modif` IS NULL THEN '1' + ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_user_valid` IS NULL THEN '1' ELSE '0' END + AS `pin__fk_user_valid`, CASE WHEN t.`is_also_delivery_note` = CAST('0' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__is_also_delivery_note`, CASE WHEN t.`localtax1` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax1`, CASE WHEN t.`localtax2` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax2`, CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, CASE WHEN t.`paye` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__paye`, CASE WHEN t.`prorata_discount` IS NULL THEN '1' ELSE '0' + END AS `pin__prorata_discount`, CASE WHEN t.`remise` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE + '0' END AS `pin__remise`, CASE WHEN t.`remise_absolue` = CAST('0' AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `pin__remise_absolue`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`retained_warranty` = CAST('0' AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty`, CASE WHEN t.`retained_warranty_date_limit` + IS NULL THEN '1' ELSE '0' END AS `pin__retained_warranty_date_limit`, CASE WHEN t.`retained_warranty_fk_cond_reglement` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty_fk_cond_reglement`, + CASE WHEN t.`revenuestamp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__revenuestamp`, + CASE WHEN t.`situation_counter` IS NULL THEN '1' ELSE '0' END AS `pin__situation_counter`, CASE + WHEN t.`situation_cycle_ref` IS NULL THEN '1' ELSE '0' END AS `pin__situation_cycle_ref`, CASE + WHEN t.`situation_final` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_final`, + CASE WHEN t.`subtype` IS NULL THEN '1' ELSE '0' END AS `pin__subtype`, CASE WHEN t.`type` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__type` FROM `llx_facture` t + rows_llx_facturedet: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_facture` + AS CHAR) AS `fk_facture`, CAST(t.`fk_parent_line` AS CHAR) AS `fk_parent_line`, CAST(t.`fk_product` + AS CHAR) AS `fk_product`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS + `description`, CAST(t.`vat_src_code` AS CHAR) AS `vat_src_code`, CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, + CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, + CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, + CAST(t.`qty` AS CHAR) AS `qty`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise` + AS CHAR) AS `remise`, CAST(t.`fk_remise_except` AS CHAR) AS `fk_remise_except`, CAST(t.`subprice` + AS CHAR) AS `subprice`, CAST(t.`subprice_ttc` AS CHAR) AS `subprice_ttc`, CAST(t.`price` AS CHAR) + AS `price`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, + CAST(t.`total_localtax1` AS CHAR) AS `total_localtax1`, CAST(t.`total_localtax2` AS CHAR) AS `total_localtax2`, + CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, CAST(t.`product_type` AS CHAR) AS `product_type`, + CAST(t.`date_start` AS CHAR) AS `date_start`, CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`info_bits` + AS CHAR) AS `info_bits`, CAST(t.`buy_price_ht` AS CHAR) AS `buy_price_ht`, CAST(t.`fk_product_fournisseur_price` + AS CHAR) AS `fk_product_fournisseur_price`, CAST(t.`batch` AS CHAR) AS `batch`, CAST(t.`fk_warehouse` + AS CHAR) AS `fk_warehouse`, CAST(t.`special_code` AS CHAR) AS `special_code`, CAST(t.`rang` AS + CHAR) AS `rang`, CAST(t.`fk_contract_line` AS CHAR) AS `fk_contract_line`, CAST(t.`fk_unit` AS + CHAR) AS `fk_unit`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_code_ventilation` + AS CHAR) AS `fk_code_ventilation`, CAST(t.`situation_percent` AS CHAR) AS `situation_percent`, + CAST(t.`fk_prev_id` AS CHAR) AS `fk_prev_id`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, + CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_subprice` + AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, + CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` + AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CAST(a2.`nom` AS CHAR) FROM `llx_societe` + a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS + `main__customer`, (SELECT (SELECT CAST(a2.`town` AS CHAR) FROM `llx_societe` a2 WHERE a2.`rowid` + = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS `main__town`, (SELECT + CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` + AS BINARY)) AS `p__lines_0_product_ref`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND t.`qty` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_0_qty`, CASE WHEN + t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE + WHEN t.`date_end` IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS + NULL THEN '1' ELSE '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_multicurrency` = (SELECT + s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' + END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE '0' END AS + `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, + CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, + CASE WHEN t.`fk_remise_except` IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE + WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` + = (SELECT s.`fk_user_author` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE + '0' END AS `pin__fk_user_author`, CASE WHEN t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__info_bits`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, + CASE WHEN t.`multicurrency_subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` + = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE + WHEN t.`price` IS NULL THEN '1' ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT + s.`type` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, + CASE WHEN t.`rang` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN + t.`remise` = (SELECT s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' + ELSE '0' END AS `pin__remise`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__situation_percent`, CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__special_code`, CASE WHEN t.`subprice` = (SELECT s.`price` FROM + `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__subprice`, CASE + WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__subprice_ttc`, + CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) + THEN '1' ELSE '0' END AS `pin__total_localtax1`, CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax2`, + CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__tva_tx`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE + CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, CASE + WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v1)s AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `p__lines_1_qty`, CASE WHEN t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE WHEN t.`date_end` IS NULL THEN '1' ELSE '0' + END AS `pin__date_end`, CASE WHEN t.`date_start` IS NULL THEN '1' ELSE '0' END AS `pin__date_start`, + CASE WHEN t.`fk_code_ventilation` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, + CASE WHEN t.`fk_contract_line` IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE + WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` + = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_prev_id`, CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, CASE WHEN t.`fk_remise_except` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE + '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` FROM `llx_facture` + s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, CASE WHEN + t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_user_modif`, + CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_warehouse`, + CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__info_bits`, + CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_tx`, + CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_tx`, + CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` FROM `llx_facture` s WHERE s.`rowid` + = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, CASE WHEN t.`multicurrency_subprice` + = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END + AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE WHEN t.`price` IS NULL THEN '1' + ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT s.`type` FROM `llx_facture` + s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, CASE WHEN t.`rang` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN t.`remise` = (SELECT + s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__remise`, + CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_percent`, + CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_percent`, + CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__special_code`, + CASE WHEN t.`subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__subprice`, CASE WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__subprice_ttc`, CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax1`, + CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) + THEN '1' ELSE '0' END AS `pin__total_localtax2`, CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM + `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx` FROM + `llx_facturedet` t + sql_query_params: + v0: + param: lines.0.qty + v1: + param: lines.1.qty +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__customer: + param: customer + p__town: + param: town + pin__close_missing_amount: + literal: '1' + pin__date_closing: + literal: '1' + pin__date_pointoftax: + literal: '1' + pin__date_valid: + literal: '1' + pin__entity: + literal: '1' + pin__fk_account: + literal: '1' + pin__fk_cond_reglement: + literal: '1' + pin__fk_fac_rec_source: + literal: '1' + pin__fk_facture_source: + literal: '1' + pin__fk_incoterms: + literal: '1' + pin__fk_mode_reglement: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_projet: + literal: '1' + pin__fk_statut: + literal: '1' + pin__fk_transport_mode: + literal: '1' + fk_user_author: + param: bot_user_id + pin__fk_user_closing: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_user_valid: + literal: '1' + pin__is_also_delivery_note: + literal: '1' + pin__localtax1: + literal: '1' + pin__localtax2: + literal: '1' + multicurrency_code: + literal: EUR + pin__multicurrency_tx: + literal: '1' + pin__paye: + literal: '1' + pin__prorata_discount: + literal: '1' + pin__remise: + literal: '1' + pin__remise_absolue: + literal: '1' + pin__remise_percent: + literal: '1' + pin__retained_warranty: + literal: '1' + pin__retained_warranty_date_limit: + literal: '1' + pin__retained_warranty_fk_cond_reglement: + literal: '1' + pin__revenuestamp: + literal: '1' + pin__situation_counter: + literal: '1' + pin__situation_cycle_ref: + literal: '1' + pin__situation_final: + literal: '1' + pin__subtype: + literal: '1' + pin__type: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__town: + param: town + p__lines_0_product_ref: + param: lines.0.product_ref + p__lines_0_qty: + literal: '1' + pin__buy_price_ht: + literal: '1' + pin__date_end: + literal: '1' + pin__date_start: + literal: '1' + pin__fk_code_ventilation: + literal: '1' + pin__fk_contract_line: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_parent_line: + literal: '1' + pin__fk_prev_id: + literal: '1' + pin__fk_product_fournisseur_price: + literal: '1' + pin__fk_remise_except: + literal: '1' + pin__fk_unit: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_warehouse: + literal: '1' + pin__info_bits: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__multicurrency_code: + literal: '1' + pin__multicurrency_subprice: + literal: '1' + pin__multicurrency_subprice_ttc: + literal: '1' + pin__price: + literal: '1' + pin__product_type: + literal: '1' + pin__rang: + literal: '1' + pin__remise: + literal: '1' + pin__remise_percent: + literal: '1' + pin__situation_percent: + literal: '1' + pin__special_code: + literal: '1' + pin__subprice: + literal: '1' + pin__subprice_ttc: + literal: '1' + pin__total_localtax1: + literal: '1' + pin__total_localtax2: + literal: '1' + pin__tva_tx: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__town: + param: town + p__lines_1_product_ref: + param: lines.1.product_ref + p__lines_1_qty: + literal: '1' + pin__buy_price_ht: + literal: '1' + pin__date_end: + literal: '1' + pin__date_start: + literal: '1' + pin__fk_code_ventilation: + literal: '1' + pin__fk_contract_line: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_parent_line: + literal: '1' + pin__fk_prev_id: + literal: '1' + pin__fk_product_fournisseur_price: + literal: '1' + pin__fk_remise_except: + literal: '1' + pin__fk_unit: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_warehouse: + literal: '1' + pin__info_bits: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__multicurrency_code: + literal: '1' + pin__multicurrency_subprice: + literal: '1' + pin__multicurrency_subprice_ttc: + literal: '1' + pin__price: + literal: '1' + pin__product_type: + literal: '1' + pin__rang: + literal: '1' + pin__remise: + literal: '1' + pin__remise_percent: + literal: '1' + pin__situation_percent: + literal: '1' + pin__special_code: + literal: '1' + pin__subprice: + literal: '1' + pin__subprice_ttc: + literal: '1' + pin__total_localtax1: + literal: '1' + pin__total_localtax2: + literal: '1' + pin__tva_tx: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_facture + mode: skip + reason: read in full, every column, by this contract's own surface + - table: llx_facturedet + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml new file mode 100644 index 0000000000..108cdf3e93 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml @@ -0,0 +1,88 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, found, fields customer, town, linesx2. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` + AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, + CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` + AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, + CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` + AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) + AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` + AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) + AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS + `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, + CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, + CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, + CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, + CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, + CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` + AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` + AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, + CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS + `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS + `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) + AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` + AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` + AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, + CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS + `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, + CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, + CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, + CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) + AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, + CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, (SELECT CAST(a1.`town` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town` FROM `llx_facture` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__customer: + param: customer + p__town: + param: town + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_facture + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml new file mode 100644 index 0000000000..74239e91f1 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml @@ -0,0 +1,426 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_product, create, fields label, price_excl_vat, ref, vat_rate. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_parent` AS CHAR) + AS `fk_parent`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS `description`, + CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`note` AS CHAR) AS `note`, CAST(t.`customcode` + AS CHAR) AS `customcode`, CAST(t.`fk_country` AS CHAR) AS `fk_country`, CAST(t.`fk_state` AS CHAR) + AS `fk_state`, CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, + CAST(t.`price_min` AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, + CAST(t.`price_base_type` AS CHAR) AS `price_base_type`, CAST(t.`price_label` AS CHAR) AS `price_label`, + CAST(t.`cost_price` AS CHAR) AS `cost_price`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, + CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, + CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, + CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, + CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, + CAST(t.`tosell` AS CHAR) AS `tosell`, CAST(t.`tobuy` AS CHAR) AS `tobuy`, CAST(t.`tobatch` AS + CHAR) AS `tobatch`, CAST(t.`sell_or_eat_by_mandatory` AS CHAR) AS `sell_or_eat_by_mandatory`, + CAST(t.`batch_mask` AS CHAR) AS `batch_mask`, CAST(t.`fk_product_type` AS CHAR) AS `fk_product_type`, + CAST(t.`duration` AS CHAR) AS `duration`, CAST(t.`seuil_stock_alerte` AS CHAR) AS `seuil_stock_alerte`, + CAST(t.`url` AS CHAR) AS `url`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`accountancy_code_sell` AS CHAR) AS `accountancy_code_sell`, + CAST(t.`accountancy_code_sell_intra` AS CHAR) AS `accountancy_code_sell_intra`, CAST(t.`accountancy_code_sell_export` + AS CHAR) AS `accountancy_code_sell_export`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`accountancy_code_buy_intra` AS CHAR) AS `accountancy_code_buy_intra`, CAST(t.`accountancy_code_buy_export` + AS CHAR) AS `accountancy_code_buy_export`, CAST(t.`partnumber` AS CHAR) AS `partnumber`, CAST(t.`net_measure` + AS CHAR) AS `net_measure`, CAST(t.`net_measure_units` AS CHAR) AS `net_measure_units`, CAST(t.`weight` + AS CHAR) AS `weight`, CAST(t.`weight_units` AS CHAR) AS `weight_units`, CAST(t.`length` AS CHAR) + AS `length`, CAST(t.`length_units` AS CHAR) AS `length_units`, CAST(t.`width` AS CHAR) AS `width`, + CAST(t.`width_units` AS CHAR) AS `width_units`, CAST(t.`height` AS CHAR) AS `height`, CAST(t.`height_units` + AS CHAR) AS `height_units`, CAST(t.`surface` AS CHAR) AS `surface`, CAST(t.`surface_units` AS + CHAR) AS `surface_units`, CAST(t.`volume` AS CHAR) AS `volume`, CAST(t.`volume_units` AS CHAR) + AS `volume_units`, CAST(t.`stockable_product` AS CHAR) AS `stockable_product`, CAST(t.`stock` + AS CHAR) AS `stock`, CAST(t.`pmp` AS CHAR) AS `pmp`, CAST(t.`fifo` AS CHAR) AS `fifo`, CAST(t.`lifo` + AS CHAR) AS `lifo`, CAST(t.`fk_default_warehouse` AS CHAR) AS `fk_default_warehouse`, CAST(t.`fk_default_bom` + AS CHAR) AS `fk_default_bom`, CAST(t.`fk_default_workstation` AS CHAR) AS `fk_default_workstation`, + CAST(t.`canvas` AS CHAR) AS `canvas`, CAST(t.`finished` AS CHAR) AS `finished`, CAST(t.`lifetime` + AS CHAR) AS `lifetime`, CAST(t.`qc_frequency` AS CHAR) AS `qc_frequency`, CAST(t.`hidden` AS CHAR) + AS `hidden`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, + CAST(t.`fk_price_expression` AS CHAR) AS `fk_price_expression`, CAST(t.`desiredstock` AS CHAR) + AS `desiredstock`, CAST(t.`fk_unit` AS CHAR) AS `fk_unit`, CAST(t.`price_autogen` AS CHAR) AS + `price_autogen`, CAST(t.`fk_project` AS CHAR) AS `fk_project`, CAST(t.`mandatory_period` AS CHAR) + AS `mandatory_period`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CASE WHEN CAST(%(v0)s + AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `p__price_excl_vat`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate`, CASE WHEN + t.`cost_price` IS NULL THEN '1' ELSE '0' END AS `pin__cost_price`, CASE WHEN t.`desiredstock` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__desiredstock`, CASE WHEN t.`entity` + = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fifo` IS NULL + THEN '1' ELSE '0' END AS `pin__fifo`, CASE WHEN t.`finished` IS NULL THEN '1' ELSE '0' END AS + `pin__finished`, CASE WHEN t.`fk_barcode_type` IS NULL THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, + CASE WHEN t.`fk_country` IS NULL THEN '1' ELSE '0' END AS `pin__fk_country`, CASE WHEN t.`fk_default_bom` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_default_bom`, CASE WHEN t.`fk_default_warehouse` IS + NULL THEN '1' ELSE '0' END AS `pin__fk_default_warehouse`, CASE WHEN t.`fk_default_workstation` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_default_workstation`, CASE WHEN t.`fk_parent` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_parent`, CASE WHEN t.`fk_price_expression` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_price_expression`, CASE WHEN t.`fk_product_type` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_product_type`, CASE WHEN t.`fk_project` IS + NULL THEN '1' ELSE '0' END AS `pin__fk_project`, CASE WHEN t.`fk_state` IS NULL THEN '1' ELSE + '0' END AS `pin__fk_state`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, + CASE WHEN t.`height` IS NULL THEN '1' ELSE '0' END AS `pin__height`, CASE WHEN t.`height_units` + IS NULL THEN '1' ELSE '0' END AS `pin__height_units`, CASE WHEN t.`hidden` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__hidden`, CASE WHEN t.`length` IS NULL THEN '1' ELSE '0' END AS + `pin__length`, CASE WHEN t.`length_units` IS NULL THEN '1' ELSE '0' END AS `pin__length_units`, + CASE WHEN t.`lifetime` IS NULL THEN '1' ELSE '0' END AS `pin__lifetime`, CASE WHEN t.`lifo` IS + NULL THEN '1' ELSE '0' END AS `pin__lifo`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`mandatory_period` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__mandatory_period`, CASE WHEN t.`net_measure` IS NULL THEN '1' ELSE + '0' END AS `pin__net_measure`, CASE WHEN t.`net_measure_units` IS NULL THEN '1' ELSE '0' END AS + `pin__net_measure_units`, CASE WHEN t.`pmp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__pmp`, CASE WHEN t.`price_autogen` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__price_autogen`, CASE WHEN t.`price_min` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' + END AS `pin__price_min`, CASE WHEN t.`price_min_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE + '0' END AS `pin__price_min_ttc`, CASE WHEN t.`qc_frequency` IS NULL THEN '1' ELSE '0' END AS `pin__qc_frequency`, + CASE WHEN t.`recuperableonly` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__recuperableonly`, + CASE WHEN t.`sell_or_eat_by_mandatory` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `pin__sell_or_eat_by_mandatory`, CASE WHEN t.`seuil_stock_alerte` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__seuil_stock_alerte`, CASE WHEN t.`stock` IS NULL THEN '1' ELSE + '0' END AS `pin__stock`, CASE WHEN t.`stockable_product` = CAST('1' AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `pin__stockable_product`, CASE WHEN t.`surface` IS NULL THEN '1' ELSE '0' END + AS `pin__surface`, CASE WHEN t.`surface_units` IS NULL THEN '1' ELSE '0' END AS `pin__surface_units`, + CASE WHEN t.`tobatch` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tobatch`, CASE + WHEN t.`tobuy` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tobuy`, CASE WHEN + t.`tosell` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tosell`, CASE WHEN t.`volume` + IS NULL THEN '1' ELSE '0' END AS `pin__volume`, CASE WHEN t.`volume_units` IS NULL THEN '1' ELSE + '0' END AS `pin__volume_units`, CASE WHEN t.`weight` IS NULL THEN '1' ELSE '0' END AS `pin__weight`, + CASE WHEN t.`weight_units` IS NULL THEN '1' ELSE '0' END AS `pin__weight_units`, CASE WHEN t.`width` + IS NULL THEN '1' ELSE '0' END AS `pin__width`, CASE WHEN t.`width_units` IS NULL THEN '1' ELSE + '0' END AS `pin__width_units` FROM `llx_product` t + sql_query_params: + v0: + param: price_excl_vat + v1: + param: vat_rate + raw_llx_actioncomm: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` + AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, + CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` + AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` + AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS + `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, + CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, + CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` + AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) + AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, + CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` + AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` + AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` + AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS + CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) + AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, + CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` + AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS + `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` + AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` + AS CHAR) AS `extraparams` FROM `llx_actioncomm` t + raw_llx_actioncomm_resources: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` + AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` + AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` + AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` + t + raw_llx_product_price: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`entity` + AS CHAR) AS `entity`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_product` AS CHAR) AS `fk_product`, + CAST(t.`date_price` AS CHAR) AS `date_price`, CAST(t.`price_level` AS CHAR) AS `price_level`, + CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, CAST(t.`price_min` + AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, CAST(t.`price_base_type` + AS CHAR) AS `price_base_type`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, CAST(t.`tva_tx` + AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, CAST(t.`localtax1_tx` + AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, CAST(t.`localtax2_tx` + AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`price_label` AS CHAR) AS `price_label`, CAST(t.`tosell` + AS CHAR) AS `tosell`, CAST(t.`price_by_qty` AS CHAR) AS `price_by_qty`, CAST(t.`fk_price_expression` + AS CHAR) AS `fk_price_expression`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_price` AS CHAR) + AS `multicurrency_price`, CAST(t.`multicurrency_price_ttc` AS CHAR) AS `multicurrency_price_ttc` + FROM `llx_product_price` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + label: + param: label + p__price_excl_vat: + literal: '1' + ref: + param: ref + p__vat_rate: + literal: '1' + pin__cost_price: + literal: '1' + pin__desiredstock: + literal: '1' + pin__entity: + literal: '1' + pin__fifo: + literal: '1' + pin__finished: + literal: '1' + pin__fk_barcode_type: + literal: '1' + pin__fk_country: + literal: '1' + pin__fk_default_bom: + literal: '1' + pin__fk_default_warehouse: + literal: '1' + pin__fk_default_workstation: + literal: '1' + pin__fk_parent: + literal: '1' + pin__fk_price_expression: + literal: '1' + pin__fk_product_type: + literal: '1' + pin__fk_project: + literal: '1' + pin__fk_state: + literal: '1' + pin__fk_unit: + literal: '1' + fk_user_author: + param: bot_user_id + fk_user_modif: + param: bot_user_id + pin__height: + literal: '1' + pin__height_units: + literal: '1' + pin__hidden: + literal: '1' + pin__length: + literal: '1' + pin__length_units: + literal: '1' + pin__lifetime: + literal: '1' + pin__lifo: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__mandatory_period: + literal: '1' + pin__net_measure: + literal: '1' + pin__net_measure_units: + literal: '1' + pin__pmp: + literal: '1' + pin__price_autogen: + literal: '1' + price_base_type: + literal: HT + pin__price_min: + literal: '1' + pin__price_min_ttc: + literal: '1' + pin__qc_frequency: + literal: '1' + pin__recuperableonly: + literal: '1' + pin__sell_or_eat_by_mandatory: + literal: '1' + pin__seuil_stock_alerte: + literal: '1' + pin__stock: + literal: '1' + pin__stockable_product: + literal: '1' + pin__surface: + literal: '1' + pin__surface_units: + literal: '1' + pin__tobatch: + literal: '1' + pin__tobuy: + literal: '1' + pin__tosell: + literal: '1' + pin__volume: + literal: '1' + pin__volume_units: + literal: '1' + pin__weight: + literal: '1' + pin__weight_units: + literal: '1' + pin__width: + literal: '1' + pin__width_units: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm + match: + code: + literal: AC_PRODUCT_CREATE + elementtype: + literal: product + entity: + literal: '1' + event_paid: + literal: '0' + fk_action: + literal: '40' + fk_parent: + literal: '0' + fk_user_action: + param: bot_user_id + fk_user_author: + param: bot_user_id + fulldayevent: + literal: '0' + percent: + literal: '-1' + priority: + literal: '0' + status: + literal: '0' + transparency: + literal: '0' + visibility: + literal: default + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources + match: + element_type: + literal: user + fk_element: + param: bot_user_id + mandatory: + literal: '0' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_product_price|1 row(s) the app adds to llx_product_price + match: + fk_user_author: + param: bot_user_id + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_product + mode: skip + reason: read in full, every column, by this contract's own surface + views: + llx_actioncomm: raw_llx_actioncomm + llx_actioncomm_resources: raw_llx_actioncomm_resources + llx_product_price: raw_llx_product_price + settle_s: 1 + settle_attempts: 5 + new_row_fields: + llx_actioncomm: + - datec + - datep + - datep2 + - durationp + - email_from + - email_msgid + - email_sender + - email_subject + - email_to + - email_tobcc + - email_tocc + - errors_to + - extraparams + - fk_element + - id + - import_key + - ip + - label + - location + - note + - recurid + - recurrule + - ref + - ref_ext + - reply_to + - tms + llx_actioncomm_resources: + - answer_status + - fk_actioncomm + - rowid + - transparency + llx_product_price: + - date_price + - default_vat_code + - entity + - fk_product + - import_key + - localtax1_tx + - localtax1_type + - localtax2_tx + - localtax2_type + - multicurrency_code + - multicurrency_tx + - price + - price_base_type + - price_by_qty + - price_label + - price_level + - price_min + - price_min_ttc + - price_ttc + - recuperableonly + - rowid + - tms + - tosell + - tva_tx + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml new file mode 100644 index 0000000000..9a555fc8e7 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml @@ -0,0 +1,104 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_product, found, fields label, price_excl_vat, ref, vat_rate. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_parent` AS CHAR) + AS `fk_parent`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS `description`, + CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`note` AS CHAR) AS `note`, CAST(t.`customcode` + AS CHAR) AS `customcode`, CAST(t.`fk_country` AS CHAR) AS `fk_country`, CAST(t.`fk_state` AS CHAR) + AS `fk_state`, CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, + CAST(t.`price_min` AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, + CAST(t.`price_base_type` AS CHAR) AS `price_base_type`, CAST(t.`price_label` AS CHAR) AS `price_label`, + CAST(t.`cost_price` AS CHAR) AS `cost_price`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, + CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, + CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, + CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, + CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, + CAST(t.`tosell` AS CHAR) AS `tosell`, CAST(t.`tobuy` AS CHAR) AS `tobuy`, CAST(t.`tobatch` AS + CHAR) AS `tobatch`, CAST(t.`sell_or_eat_by_mandatory` AS CHAR) AS `sell_or_eat_by_mandatory`, + CAST(t.`batch_mask` AS CHAR) AS `batch_mask`, CAST(t.`fk_product_type` AS CHAR) AS `fk_product_type`, + CAST(t.`duration` AS CHAR) AS `duration`, CAST(t.`seuil_stock_alerte` AS CHAR) AS `seuil_stock_alerte`, + CAST(t.`url` AS CHAR) AS `url`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`accountancy_code_sell` AS CHAR) AS `accountancy_code_sell`, + CAST(t.`accountancy_code_sell_intra` AS CHAR) AS `accountancy_code_sell_intra`, CAST(t.`accountancy_code_sell_export` + AS CHAR) AS `accountancy_code_sell_export`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`accountancy_code_buy_intra` AS CHAR) AS `accountancy_code_buy_intra`, CAST(t.`accountancy_code_buy_export` + AS CHAR) AS `accountancy_code_buy_export`, CAST(t.`partnumber` AS CHAR) AS `partnumber`, CAST(t.`net_measure` + AS CHAR) AS `net_measure`, CAST(t.`net_measure_units` AS CHAR) AS `net_measure_units`, CAST(t.`weight` + AS CHAR) AS `weight`, CAST(t.`weight_units` AS CHAR) AS `weight_units`, CAST(t.`length` AS CHAR) + AS `length`, CAST(t.`length_units` AS CHAR) AS `length_units`, CAST(t.`width` AS CHAR) AS `width`, + CAST(t.`width_units` AS CHAR) AS `width_units`, CAST(t.`height` AS CHAR) AS `height`, CAST(t.`height_units` + AS CHAR) AS `height_units`, CAST(t.`surface` AS CHAR) AS `surface`, CAST(t.`surface_units` AS + CHAR) AS `surface_units`, CAST(t.`volume` AS CHAR) AS `volume`, CAST(t.`volume_units` AS CHAR) + AS `volume_units`, CAST(t.`stockable_product` AS CHAR) AS `stockable_product`, CAST(t.`stock` + AS CHAR) AS `stock`, CAST(t.`pmp` AS CHAR) AS `pmp`, CAST(t.`fifo` AS CHAR) AS `fifo`, CAST(t.`lifo` + AS CHAR) AS `lifo`, CAST(t.`fk_default_warehouse` AS CHAR) AS `fk_default_warehouse`, CAST(t.`fk_default_bom` + AS CHAR) AS `fk_default_bom`, CAST(t.`fk_default_workstation` AS CHAR) AS `fk_default_workstation`, + CAST(t.`canvas` AS CHAR) AS `canvas`, CAST(t.`finished` AS CHAR) AS `finished`, CAST(t.`lifetime` + AS CHAR) AS `lifetime`, CAST(t.`qc_frequency` AS CHAR) AS `qc_frequency`, CAST(t.`hidden` AS CHAR) + AS `hidden`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, + CAST(t.`fk_price_expression` AS CHAR) AS `fk_price_expression`, CAST(t.`desiredstock` AS CHAR) + AS `desiredstock`, CAST(t.`fk_unit` AS CHAR) AS `fk_unit`, CAST(t.`price_autogen` AS CHAR) AS + `price_autogen`, CAST(t.`fk_project` AS CHAR) AS `fk_project`, CAST(t.`mandatory_period` AS CHAR) + AS `mandatory_period`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CASE WHEN CAST(%(v0)s + AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `p__price_excl_vat`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate` FROM `llx_product` + t + sql_query_params: + v0: + param: price_excl_vat + v1: + param: vat_rate +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + label: + param: label + p__price_excl_vat: + literal: '1' + ref: + param: ref + p__vat_rate: + literal: '1' + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_product + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml new file mode 100644 index 0000000000..0536988a03 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml @@ -0,0 +1,101 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_customer, found, fields customer, new_address, new_email, new_phone, town. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + nom: + param: customer + address: + param: new_address + email: + param: new_email + phone: + param: new_phone + kind: phone + town: + param: town + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml new file mode 100644 index 0000000000..788c0c1431 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml @@ -0,0 +1,218 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_customer, update, fields customer, new_address, new_email, new_phone, town. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` + t + raw_llx_actioncomm: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` + AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, + CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` + AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` + AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS + `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, + CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, + CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` + AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) + AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, + CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` + AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` + AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` + AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS + CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) + AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, + CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` + AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS + `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` + AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` + AS CHAR) AS `extraparams` FROM `llx_actioncomm` t + raw_llx_actioncomm_resources: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` + AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` + AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` + AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + nom: + param: customer + address: + param: new_address + email: + param: new_email + phone: + param: new_phone + kind: phone + town: + param: town + expected_count: 1 +- kind: record_written + probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm + match: + entity: + literal: '1' + event_paid: + literal: '0' + fk_action: + literal: '40' + fk_parent: + literal: '0' + fk_user_action: + param: bot_user_id + fk_user_author: + param: bot_user_id + fulldayevent: + literal: '0' + percent: + literal: '-1' + priority: + literal: '0' + status: + literal: '0' + transparency: + literal: '0' + visibility: + literal: default + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources + match: + element_type: + literal: user + fk_element: + param: bot_user_id + mandatory: + literal: '0' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: + - address + - email + - phone + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: + llx_actioncomm: raw_llx_actioncomm + llx_actioncomm_resources: raw_llx_actioncomm_resources + settle_s: 1 + settle_attempts: 5 + new_row_fields: + llx_actioncomm: + - code + - datec + - datep + - datep2 + - durationp + - elementtype + - email_from + - email_msgid + - email_sender + - email_subject + - email_to + - email_tobcc + - email_tocc + - errors_to + - extraparams + - fk_soc + - id + - import_key + - ip + - label + - location + - note + - recurid + - recurrule + - ref + - ref_ext + - reply_to + - tms + llx_actioncomm_resources: + - answer_status + - fk_actioncomm + - rowid + - transparency + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml new file mode 100644 index 0000000000..2d06c7591d --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml @@ -0,0 +1,99 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_customer, found, fields customer, new_email, new_phone, town. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + nom: + param: customer + email: + param: new_email + phone: + param: new_phone + kind: phone + town: + param: town + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml new file mode 100644 index 0000000000..6d51c78306 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml @@ -0,0 +1,215 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_customer, update, fields customer, new_email, new_phone, town. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` + t + raw_llx_actioncomm: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` + AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, + CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` + AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` + AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS + `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, + CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, + CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` + AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) + AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, + CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` + AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` + AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` + AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS + CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) + AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, + CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` + AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS + `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` + AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` + AS CHAR) AS `extraparams` FROM `llx_actioncomm` t + raw_llx_actioncomm_resources: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` + AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` + AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` + AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + nom: + param: customer + email: + param: new_email + phone: + param: new_phone + kind: phone + town: + param: town + expected_count: 1 +- kind: record_written + probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm + match: + entity: + literal: '1' + event_paid: + literal: '0' + fk_action: + literal: '40' + fk_parent: + literal: '0' + fk_user_action: + param: bot_user_id + fk_user_author: + param: bot_user_id + fulldayevent: + literal: '0' + percent: + literal: '-1' + priority: + literal: '0' + status: + literal: '0' + transparency: + literal: '0' + visibility: + literal: default + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources + match: + element_type: + literal: user + fk_element: + param: bot_user_id + mandatory: + literal: '0' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: + - email + - phone + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: + llx_actioncomm: raw_llx_actioncomm + llx_actioncomm_resources: raw_llx_actioncomm_resources + settle_s: 1 + settle_attempts: 5 + new_row_fields: + llx_actioncomm: + - code + - datec + - datep + - datep2 + - durationp + - elementtype + - email_from + - email_msgid + - email_sender + - email_subject + - email_to + - email_tobcc + - email_tocc + - errors_to + - extraparams + - fk_soc + - id + - import_key + - ip + - label + - location + - note + - recurid + - recurrule + - ref + - ref_ext + - reply_to + - tms + llx_actioncomm_resources: + - answer_status + - fk_actioncomm + - rowid + - transparency + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml new file mode 100644 index 0000000000..be7c828bd3 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml @@ -0,0 +1,311 @@ +schema_version: openadapt.contract-manifest/v1 +dialect: mysql +database: dolidb +actor_params: +- bot_user_id +questions_open: [] +kinds: + create_customer: + mode: create + main_table: llx_societe + parameters: + - name + - address + - zip + - town + - country + - phone + - email + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - address + - country + - email + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml + found: contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml + create_product: + mode: create + main_table: llx_product + parameters: + - ref + - label + - price_excl_vat + - vat_rate + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - label + - price_excl_vat + - ref + - vat_rate + lists: {} + dispatch_value: null + create: contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml + found: contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml + create_draft_invoice: + mode: create + main_table: llx_facture + parameters: + - customer + - town + - invoice_date + - lines + identity: [] + lists: + - lines + dispatch: null + variants: + - fields: + - customer + - invoice_date + - town + lists: + lines: 1 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx1.678821c0.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx1.678821c0.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 2 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 3 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx3.2f437a2c.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx3.2f437a2c.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 4 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx4.aea56f08.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx4.aea56f08.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 5 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx5.58afdd66.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx5.58afdd66.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 6 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx6.edf87893.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx6.edf87893.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 7 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx7.086ca1eb.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx7.086ca1eb.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 8 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx8.8d4e9b1f.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx8.8d4e9b1f.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 9 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx9.042138e5.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx9.042138e5.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 10 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx10.f6fa907c.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx10.f6fa907c.found.yaml + update_customer: + mode: update + main_table: llx_societe + parameters: + - customer + - town + - new_phone + - new_email + - new_address + - new_zip + identity: + - customer + - town + lists: [] + dispatch: null + variants: + - fields: + - customer + - new_phone + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_phone+town.e481e632.update.yaml + found: contracts/update_customer/customer+new_phone+town.e481e632.found.yaml + - fields: + - customer + - new_email + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_email+town.014f1cf4.update.yaml + found: contracts/update_customer/customer+new_email+town.014f1cf4.found.yaml + - fields: + - customer + - new_address + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+town.7b5a4e67.update.yaml + found: contracts/update_customer/customer+new_address+town.7b5a4e67.found.yaml + - fields: + - customer + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_zip+town.5089de78.update.yaml + found: contracts/update_customer/customer+new_zip+town.5089de78.found.yaml + - fields: + - customer + - new_email + - new_phone + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml + found: contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml + - fields: + - customer + - new_address + - new_phone + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_phone+town.be3325cd.update.yaml + found: contracts/update_customer/customer+new_address+new_phone+town.be3325cd.found.yaml + - fields: + - customer + - new_phone + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_phone+new_zip+town.00a25668.update.yaml + found: contracts/update_customer/customer+new_phone+new_zip+town.00a25668.found.yaml + - fields: + - customer + - new_address + - new_email + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_email+town.304fdd4f.update.yaml + found: contracts/update_customer/customer+new_address+new_email+town.304fdd4f.found.yaml + - fields: + - customer + - new_email + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_email+new_zip+town.adf5982d.update.yaml + found: contracts/update_customer/customer+new_email+new_zip+town.adf5982d.found.yaml + - fields: + - customer + - new_address + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_zip+town.8d26c1c7.update.yaml + found: contracts/update_customer/customer+new_address+new_zip+town.8d26c1c7.found.yaml + - fields: + - customer + - new_address + - new_email + - new_phone + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml + found: contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml + - fields: + - customer + - new_email + - new_phone + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_email+new_phone+new_zip+town.d9b5d1c2.update.yaml + found: contracts/update_customer/customer+new_email+new_phone+new_zip+town.d9b5d1c2.found.yaml + - fields: + - customer + - new_address + - new_phone + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_phone+new_zip+town.998004a8.update.yaml + found: contracts/update_customer/customer+new_address+new_phone+new_zip+town.998004a8.found.yaml + - fields: + - customer + - new_address + - new_email + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_email+new_zip+town.e4f14417.update.yaml + found: contracts/update_customer/customer+new_address+new_email+new_zip+town.e4f14417.found.yaml + - fields: + - customer + - new_address + - new_email + - new_phone + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_email+new_phone+new_zip+town.0d2342b6.update.yaml + found: contracts/update_customer/customer+new_address+new_email+new_phone+new_zip+town.0d2342b6.found.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json new file mode 100644 index 0000000000..be5ce10a31 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json @@ -0,0 +1,157 @@ +{ + "applied": [], + "dialect": "mysql", + "tables": 387, + "questions": [ + { + "number": 1, + "key": "bookkeeping", + "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", + "proposal": "accept", + "answered": true + }, + { + "number": 2, + "key": "recheck_after_s", + "text": "How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `.", + "proposal": 300, + "answered": true + }, + { + "number": 3, + "key": "locations.create_draft_invoice", + "text": "Kind `create_draft_invoice`: `invoice_date` is in several places: [0] llx_facture.datef (date); [1] llx_facture.date_lim_reglement (date) Answer `locations.create_draft_invoice: {: }` to pick one.", + "proposal": null, + "answered": true + } + ], + "kinds": { + "create_customer": { + "mode": "create", + "main_table": "llx_societe", + "locations": { + "address": "llx_societe.address (exact)", + "country": "llx_societe.fk_pays -> llx_c_country.label (exact)", + "email": "llx_societe.email (exact)", + "name": "llx_societe.nom (exact)", + "phone": "llx_societe.phone (phone)", + "town": "llx_societe.town (exact)", + "zip": "llx_societe.zip (exact)" + }, + "placed_by_name": [], + "demonstrations": 2, + "children": {}, + "lists": {}, + "side_rows": [ + { + "table": "llx_actioncomm", + "count": 1, + "unless": null + }, + { + "table": "llx_actioncomm_resources", + "count": 1, + "unless": null + } + ], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 1, + "rejected": [] + }, + "create_product": { + "mode": "create", + "main_table": "llx_product", + "locations": { + "label": "llx_product.label (exact)", + "price_excl_vat": "llx_product.price (number)", + "ref": "llx_product.ref (exact)", + "vat_rate": "llx_product.tva_tx (number)" + }, + "placed_by_name": [], + "demonstrations": 2, + "children": {}, + "lists": {}, + "side_rows": [ + { + "table": "llx_actioncomm", + "count": 1, + "unless": null + }, + { + "table": "llx_actioncomm_resources", + "count": 1, + "unless": null + }, + { + "table": "llx_product_price", + "count": 1, + "unless": null + } + ], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 1, + "rejected": [] + }, + "create_draft_invoice": { + "mode": "create", + "main_table": "llx_facture", + "locations": { + "customer": "llx_facture.fk_soc -> llx_societe.nom (exact)", + "invoice_date": "llx_facture.datef (date)", + "town": "llx_facture.fk_soc -> llx_societe.town (exact)" + }, + "placed_by_name": [], + "demonstrations": 2, + "children": {}, + "lists": { + "lines": "llx_facturedet" + }, + "side_rows": [], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 10, + "rejected": [] + }, + "update_customer": { + "mode": "update", + "main_table": "llx_societe", + "locations": { + "customer": "llx_societe.nom (exact)", + "new_address": "llx_societe.address (exact)", + "new_email": "llx_societe.email (exact)", + "new_phone": "llx_societe.phone (phone)", + "new_zip": "llx_societe.zip (exact)", + "town": "llx_societe.town (exact)" + }, + "placed_by_name": [ + "new_address", + "new_zip" + ], + "demonstrations": 2, + "children": {}, + "lists": {}, + "side_rows": [ + { + "table": "llx_actioncomm", + "count": 1, + "unless": null + }, + { + "table": "llx_actioncomm_resources", + "count": 1, + "unless": null + } + ], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 15, + "rejected": [] + } + } +} \ No newline at end of file diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 58632ec7ab..a6f4ce993d 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1791,8 +1791,21 @@ def propose( book.setdefault(t, set()).update(cols) for t, cols in dict(ans.get("columns_remove", {})).items(): book[t] = book.get(t, set()) - set(cols) - if logs or book or unchecked: + sides = sorted( + {f"{s.table} ({s.count} per write)" for p in plans for s in p.sides} + ) + for p in plans: # a table the person calls a log is no longer counted + p.sides = [s for s in p.sides if s.table not in logs] + if logs or book or unchecked or sides: lines = [] + if sides: + lines.append( + "Rows the app adds beside each write, expected exactly that many " + "times (a second save adds a second one, which stops the write; " + "list a table under logs_add to allow any number): " + + ", ".join(sides) + + "." + ) if logs: lines.append( "Tables treated as the app's own logs (rows may be added, existing " From b0598877e7e451ba803df5199890efaa2b634b3c Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:32:49 -0400 Subject: [PATCH 12/94] WIP: empty child rows, locations shared across kinds, exact-name update fields --- .../generate_contracts/demo_redmine.py | 4 +- .../generate/verify_contract.py | 84 +++++++++++++++++-- .../tests/test_generate_verify_contract.py | 7 +- 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/packages/flow/benchmark/generate_contracts/demo_redmine.py b/packages/flow/benchmark/generate_contracts/demo_redmine.py index 6f643913ad..e98b53298a 100644 --- a/packages/flow/benchmark/generate_contracts/demo_redmine.py +++ b/packages/flow/benchmark/generate_contracts/demo_redmine.py @@ -111,7 +111,9 @@ def create_payload(v: dict) -> dict: if kind == "ensure_issue" and int(one( "SELECT COUNT(*) FROM issues i JOIN projects p ON p.id=i.project_id " "WHERE p.name=%s AND i.subject=%s", (v["project"], v["subject"]))): # fmt: skip - continue + # Already there: demonstrate a create with values of our own. + v = {**v, "subject": "Generator demo: rotate the backup keys", + "priority": "Low", "assignee": "Jon Park"} demo( work, kind, diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index a6f4ce993d..7595f8beee 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -191,6 +191,9 @@ class ChildGroup: #: row also holds the title): with one of them given and this one not, #: the row is still expected, with this value empty. co_owners: list[str] = field(default_factory=list) + #: The app adds this row with an empty value when the task leaves the + #: parameter out (Redmine's custom field values): expected then too. + empty_when_null: bool = False @dataclass @@ -232,6 +235,8 @@ class KindPlan: removed: dict[str, int] = field(default_factory=dict) #: Parameters placed by name, not by a demonstration. by_name: list[str] = field(default_factory=list) + #: Parameters placed where another kind's demonstrations found them. + shared: list[str] = field(default_factory=list) def _scalars(values: Mapping[str, Any]) -> dict[str, Any]: @@ -398,6 +403,37 @@ def analyze( return plan +def _share_locations(schema: Schema, plans: Sequence[KindPlan], kinds: Sequence[KindSpec]) -> None: + """An update parameter no example of its kind gives (a priority), placed + where another kind's demonstrations found the parameter of the same name + on the same main table (``issues.priority_id -> enumerations.name``). + Placed this way, it replaces a placement by name.""" + + for plan in plans: + if plan.mode != "update" or not plan.anchor: + continue + for name in plan.params: + if name in plan.identity or name == plan.dispatch: + continue + if name in plan.locations and name not in plan.by_name: + continue + for other in plans: + if other is plan or other.anchor != plan.anchor: + continue + loc = other.locations.get(name) + if loc is None or name in other.by_name: + continue + if loc.hops and loc.hops[0].kind != "fk": + continue + plan.locations[name] = loc + if name in plan.by_name: + plan.by_name.remove(name) + plan.shared.append(name) + if name not in plan.changes: + plan.changes.append(name) + break + + def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: """An update parameter no demonstration gives (``new_zip``), placed at the main table's column of the same name after a leading ``new_`` or @@ -408,8 +444,6 @@ def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: if plan.mode != "update" or not plan.anchor: return None name = re.sub(r"^(new|set)_", "", param) - if name == param: - return None col = schema.tables[plan.anchor].column(name) if col is None or name in schema.tables[plan.anchor].primary_key: return None @@ -603,6 +637,8 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N per_table.setdefault(ch.table, []) counts.setdefault(ch.table, [0] * len(plan.demos)) continue + if ch.table in child_tables and _is_empty_param_row(schema, plan, d, a, ch): + continue if _is_ref_row(schema, plan, d, ch): continue per_table.setdefault(ch.table, []).append(ch.after or {}) @@ -660,6 +696,33 @@ def _is_param_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> boo return False +def _is_empty_param_row( + schema: Schema, plan: KindPlan, d: Demo, a: RowChange, ch: RowChange +) -> bool: + """A row the app added for a parameter the task left out: linked to the + main record, holding every constant of that parameter's rows, and an + empty value where the parameter's value goes.""" + + if ch.after is None or a.after is None: + return False + for p, g in plan.children.items(): + if g.table != ch.table or d.values.get(p) is not None: + continue + if ch.after.get(g.link.column) != a.after.get(g.link.ref_column): + continue + rel = g.params[p] + cols = rel.columns if not rel.hops else (rel.hops[0].column,) + if any(ch.after.get(c) not in (None, "") for c in cols): + continue + consts = [ + (c, r.value) for c, r in g.roles.items() if r.role == "constant" + ] + if consts and all(ch.after.get(c) == v for c, v in consts): + g.empty_when_null = True + return True + return False + + def _is_ref_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: """A row the write added that a parameter's location points to (a new email address row a link row names): declared by the value it holds.""" @@ -985,7 +1048,9 @@ def build_contract( groups = [ (p, g) for p, g in plan.children.items() - if p in fields or (plan.mode == "create" and set(g.co_owners) & fields) + if p in fields + or g.empty_when_null + or (plan.mode == "create" and set(g.co_owners) & fields) ] groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] for p, g in groups: @@ -1455,11 +1520,14 @@ def flatten(values: Mapping[str, Any]) -> dict[str, Any]: return out -def sample_values(plan: KindPlan, variant: Variant) -> dict[str, Any]: - """Values to dry-read a contract with: the demonstration's own.""" +def sample_values( + plan: KindPlan, variant: Variant, others: Sequence[Demo] = () +) -> dict[str, Any]: + """Values to dry-read a contract with: the demonstrations' own (another + kind's, for a parameter placed from that kind).""" out: dict[str, Any] = {} - for d in plan.demos: + for d in [*plan.demos, *others]: for k, v in d.values.items(): if v is not None and k not in out: out[k] = v @@ -1731,6 +1799,7 @@ def propose( plans = [analyze(schema, k, demos, actor_params) for k in kinds] questions: list[Question] = [] + _share_locations(schema, plans, kinds) location_questions: list[Question] = [] # Q: per kind, a value the generator couldn't place. for p in plans: @@ -1951,6 +2020,7 @@ def propose( k: v.describe(p.anchor) for k, v in sorted(p.locations.items()) }, "placed_by_name": sorted(p.by_name), + "placed_from_another_kind": sorted(p.shared), "demonstrations": len(p.demos), "children": {k: g.table for k, g in p.children.items()}, "lists": {k: g.table for k, g in p.lists.items()}, @@ -2009,7 +2079,7 @@ def propose( why = why or guard_problem(s["sql_query"]) if why is None and validate: why = dry_read( - path, flatten(sample_values(p, v)), dialect == "mysql" + path, flatten(sample_values(p, v, demos)), dialect == "mysql" ) if why is not None: path.unlink() diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index d3cf81c4b4..20475c3bf8 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -334,9 +334,10 @@ def test_questions_and_contracts(generated: dict[str, Any]) -> None: assert kinds["update_issue"]["mode"] == "update" assert kinds["create_issue"]["emitted"] > 0, kinds["create_issue"] # Four change fields the examples give (status, assignee, done ratio, - # notes): every non-empty subset gets a contract. No example gives a due - # date, so no contract changes one. - assert kinds["update_issue"]["emitted"] == 15, json.dumps( + # notes), and the due date that create_issue's demonstrations placed on + # the same table: every non-empty subset gets a contract. + assert kinds["update_issue"]["placed_from_another_kind"] == ["due_date"] + assert kinds["update_issue"]["emitted"] == 31, json.dumps( kinds["update_issue"], indent=1 ) assert "journal_details" in kinds["update_issue"]["logs"] From 1e863465d24561255697fde268589a00e8218413 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:34:00 -0400 Subject: [PATCH 13/94] WIP: typed sample values for fields placed by name --- packages/flow/openadapt_flow/generate/verify_contract.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 7595f8beee..65b777989c 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1537,6 +1537,13 @@ def sample_values( items = out.get(p) if isinstance(out.get(p), list) else [] if items: out[p] = [items[i % len(items)] for i in range(n)] + for k in variant.fields: + loc = plan.locations.get(k) + if k not in out and loc is not None: + # Placed by name, so no demonstration gave a value: any value of + # the right type reads the same columns. + out[k] = {"number": 1, "minutes": 1, "date": "2026-01-01", + "datetime": "2026-01-01 00:00"}.get(loc.rule, "x") # fmt: skip keep = set(variant.fields) | set(variant.lists) return {k: v for k, v in out.items() if k in keep} From bd284fdb32c3b116d1f884fac2cf863635c218b4 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:45:40 -0400 Subject: [PATCH 14/94] WIP: nullable fields optional on create; date parts unchecked; Postgres bookkeeping ignored on every surface --- .../openadapt_flow/generate/demo_analysis.py | 33 +++++++++++++++++++ .../generate/verify_contract.py | 25 ++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 02d8bdba7d..50283d7dd5 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -589,6 +589,10 @@ def column_roles( ): out[name] = ColumnRole("varies", detail={"why": "holds the row's own key"}) continue + part = _date_part(values, [d for d, _ in rows]) + if part is not None: + out[name] = ColumnRole("varies", detail={"why": f"the {part} of a date the task gives"}) + continue copied = _copied(schema, t, name, rows) if copied is not None: out[name] = ColumnRole("copied", detail=copied) @@ -618,6 +622,35 @@ def column_roles( return out +def _date_part(values: Sequence[Optional[str]], demos: Sequence["Demo"]) -> Optional[str]: + """Is the column the year, month, ISO week or day of one date value the + task gives, in every row? (Redmine stores a time entry's ``tyear``, + ``tmonth`` and ``tweek``.) The app computes it, so it isn't pinned.""" + + parts = { + "year": lambda d: d.year, + "month": lambda d: d.month, + "week": lambda d: d.isocalendar()[1], + "day": lambda d: d.day, + } + for label, fn in parts.items(): + ok = bool(values) + for v, demo in zip(values, demos): + dates = [] + for x in demo.values.values(): + if isinstance(x, str) and re.match(r"^\d{4}-\d{2}-\d{2}", x): + try: + dates.append(_dt.date.fromisoformat(x[:10])) + except ValueError: + pass + if v is None or not any(str(fn(dd)) == v for dd in dates): + ok = False + break + if ok: + return label + return None + + def _copied( schema: Schema, t: Table, diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 65b777989c..95912773e3 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -51,6 +51,7 @@ Demo, Hop, Location, + _hop_cost, _rank, best, column_roles, @@ -393,6 +394,20 @@ def analyze( if plan.mode == "update" and name not in plan.identity: plan.changes.append(name) progress = True + if plan.mode == "create": + # A create task may leave out any value whose place can be empty: a + # nullable column, a nullable key column, or a row beside the record. + anchor_t = schema.tables[plan.anchor] + for p, loc in plan.locations.items(): + col = None + if not loc.hops and len(loc.columns) == 1: + col = anchor_t.column(loc.columns[0]) + elif loc.hops and loc.hops[0].kind == "fk": + col = anchor_t.column(loc.hops[0].column) + if (col is not None and col.nullable) or (loc.hops and loc.hops[0].kind == "child"): + if p not in plan.optional: + plan.optional.append(p) + plan.optional.sort() if plan.mode == "update" and not plan.identity: plan.refused.append( "no parameter names the record before the write, so the update's " @@ -441,7 +456,7 @@ def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: type. Reported as placed by name; a demonstration that gives it replaces this.""" - if plan.mode != "update" or not plan.anchor: + if not plan.anchor: return None name = re.sub(r"^(new|set)_", "", param) col = schema.tables[plan.anchor].column(name) @@ -498,7 +513,7 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: if d.values.get(q) is not None and not isinstance(d.values.get(q), (list, dict)) ] - if hits and all(h and h[0].hops for h in hits): + if hits and all(h and h[0].hops and _hop_cost(h[0].hops[0]) <= 1 for h in hits): co.append(q) explained |= {h[0].hops[0].column for h in hits} plan.children[p] = ChildGroup( @@ -1167,7 +1182,11 @@ def build_contract( # against them, and the whole-database frame covers every other table. ignore = sorted( {c for cols in bookkeeping.values() for c in cols} - & _all_columns(schema, named_tables) + & ( + _all_columns(schema, named_tables) + if schema.dialect == "mysql" + else _all_columns(schema, set(schema.tables)) + ) ) frame: dict[str, Any] = {} if ignore: From af9a85c54d9c97756bd12fc278a0206c62866a3c Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:03:27 -0400 Subject: [PATCH 15/94] WIP: found contracts check rows beside the record; unique pin columns --- .../generate/verify_contract.py | 99 ++++++++++++------- 1 file changed, 65 insertions(+), 34 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 95912773e3..ed25c8e436 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -404,7 +404,9 @@ def analyze( col = anchor_t.column(loc.columns[0]) elif loc.hops and loc.hops[0].kind == "fk": col = anchor_t.column(loc.hops[0].column) - if (col is not None and col.nullable) or (loc.hops and loc.hops[0].kind == "child"): + if (col is not None and col.nullable) or ( + loc.hops and loc.hops[0].kind == "child" + ): if p not in plan.optional: plan.optional.append(p) plan.optional.sort() @@ -418,7 +420,9 @@ def analyze( return plan -def _share_locations(schema: Schema, plans: Sequence[KindPlan], kinds: Sequence[KindSpec]) -> None: +def _share_locations( + schema: Schema, plans: Sequence[KindPlan], kinds: Sequence[KindSpec] +) -> None: """An update parameter no example of its kind gives (a priority), placed where another kind's demonstrations found the parameter of the same name on the same main table (``issues.priority_id -> enumerations.name``). @@ -513,7 +517,9 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: if d.values.get(q) is not None and not isinstance(d.values.get(q), (list, dict)) ] - if hits and all(h and h[0].hops and _hop_cost(h[0].hops[0]) <= 1 for h in hits): + if hits and all( + h and h[0].hops and _hop_cost(h[0].hops[0]) <= 1 for h in hits + ): co.append(q) explained |= {h[0].hops[0].column for h in hits} plan.children[p] = ChildGroup( @@ -729,9 +735,7 @@ def _is_empty_param_row( cols = rel.columns if not rel.hops else (rel.hops[0].column,) if any(ch.after.get(c) not in (None, "") for c in cols): continue - consts = [ - (c, r.value) for c, r in g.roles.items() if r.role == "constant" - ] + consts = [(c, r.value) for c, r in g.roles.items() if r.role == "constant"] if consts and all(ch.after.get(c) == v for c, v in consts): g.empty_when_null = True return True @@ -1059,13 +1063,14 @@ def build_contract( # Rows beside the main record that hold a value (a note, a custom field, # an email address link), and list items (invoice lines). - if flavor != "found": + found = flavor == "found" + if True: groups = [ (p, g) for p, g in plan.children.items() if p in fields - or g.empty_when_null - or (plan.mode == "create" and set(g.co_owners) & fields) + or (not found and g.empty_when_null) + or (not found and plan.mode == "create" and set(g.co_owners) & fields) ] groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] for p, g in groups: @@ -1117,23 +1122,24 @@ def build_contract( else: m[key] = _value_expr(pname, loc.rule) for col, role in sorted(g.roles.items()): - if role.role == "link": + if role.role == "link" or found: continue _pin(b, surf, g.table, col, role, m, settings.actor_params) - effects.append( - { - "kind": "record_written", - "probe": f"surface={sname}|a row beside the main record holds {p}", - "match": m, - "expected_count": 1, - "count_new_only": True, - } - ) + effect = { + "kind": "record_written", + "probe": f"surface={sname}|a row beside the main record holds {p}", + "match": m, + "expected_count": 1, + } + if not found: + # Found: the row is already there, and nothing changes. + effect["count_new_only"] = True + effects.append(effect) if plan.mode == "update": target_fields.update(g.changed_columns) # A new row a link row names (a new email address). for p, ref in plan.added_refs.items(): - if p not in fields: + if p not in fields or found: continue effects.append( _raw_effect( @@ -1149,7 +1155,7 @@ def build_contract( ) ) for side in plan.sides: - if side.unless is not None and side.unless in fields: + if found or (side.unless is not None and side.unless in fields): continue m = {} for col, role in sorted(side.roles.items()): @@ -1301,17 +1307,34 @@ def _pin( if role.value is not None and column is not None and column.kind == "text": match.setdefault(col, {"literal": role.value}) return - alias = f"pin__{_alias(col)}" - surf.columns.append((alias, b.pin("t", table, col, role.value))) + alias = _add_column( + surf, f"pin__{_alias(col)}", b.pin("t", table, col, role.value) + ) match[alias] = {"literal": "1"} elif role.role == "actor": match.setdefault(col, {"param": role.detail["param"]}) elif role.role == "copied": - alias = f"pin__{_alias(col)}" - surf.columns.append((alias, b.copied("t", role.detail, table, col))) + alias = _add_column( + surf, f"pin__{_alias(col)}", b.copied("t", role.detail, table, col) + ) match[alias] = {"literal": "1"} +def _add_column(surf: Surface, alias: str, expr: str) -> str: + """Add a computed column; the same name with another expression (two + rows of one table pinned to different constants) gets its own name.""" + + for a, e in surf.columns: + if a == alias and e == expr: + return alias + if any(a == alias for a, _ in surf.columns): + alias = f"{alias}__{hashlib.sha256(expr.encode()).hexdigest()[:6]}" + if any(a == alias for a, _ in surf.columns): + return alias + surf.columns.append((alias, expr)) + return alias + + def _raw_effect( b: SqlBuilder, schema: Schema, @@ -1886,9 +1909,7 @@ def propose( book.setdefault(t, set()).update(cols) for t, cols in dict(ans.get("columns_remove", {})).items(): book[t] = book.get(t, set()) - set(cols) - sides = sorted( - {f"{s.table} ({s.count} per write)" for p in plans for s in p.sides} - ) + sides = sorted({f"{s.table} ({s.count} per write)" for p in plans for s in p.sides}) for p in plans: # a table the person calls a log is no longer counted p.sides = [s for s in p.sides if s.table not in logs] if logs or book or unchecked or sides: @@ -2243,14 +2264,24 @@ def _exists(contract: Any, params: Mapping[str, Any]) -> bool: from openadapt_flow.runtime.effects.effect import record_matches from openadapt_flow.verify_run import _build_verifier + from openadapt_flow.runtime.effects.surfaces import surface_of + verifier = _build_verifier(contract.config, params) state = verifier.capture_pre_state() - effect = contract.config.effects[0].resolve(params) - sub = (state.detail or {}).get("record", {}) - if not sub.get("reachable"): - raise GenerateError("the store could not be read to choose the contract") - selector = {k: v for k, v in effect.match.items()} - return any(record_matches(dict(r), selector) for r in sub.get("records", [])) + # Every effect of the found contract (the main record, and each row + # beside it that holds a value) must already hold. + for effect in contract.config.effects: + bound = effect.resolve(params) + sub = (state.detail or {}).get(surface_of(effect) or "record", {}) + if not sub.get("reachable"): + raise GenerateError("the store could not be read to choose the contract") + selector = dict(bound.match) + hits = sum( + 1 for r in sub.get("records", []) if record_matches(dict(r), selector) + ) + if hits != bound.expected_count: + return False + return True # -- CLI ------------------------------------------------------------------------------------ From 86c64885e9bf5b9839066e840e74b887ce7a6486 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:06:47 -0400 Subject: [PATCH 16/94] WIP: tidy found handling --- .../openadapt_flow/generate/demo_analysis.py | 8 +- .../generate/verify_contract.py | 220 +++++++++--------- 2 files changed, 114 insertions(+), 114 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 50283d7dd5..c3c3b9bfdb 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -591,7 +591,9 @@ def column_roles( continue part = _date_part(values, [d for d, _ in rows]) if part is not None: - out[name] = ColumnRole("varies", detail={"why": f"the {part} of a date the task gives"}) + out[name] = ColumnRole( + "varies", detail={"why": f"the {part} of a date the task gives"} + ) continue copied = _copied(schema, t, name, rows) if copied is not None: @@ -622,7 +624,9 @@ def column_roles( return out -def _date_part(values: Sequence[Optional[str]], demos: Sequence["Demo"]) -> Optional[str]: +def _date_part( + values: Sequence[Optional[str]], demos: Sequence["Demo"] +) -> Optional[str]: """Is the column the year, month, ISO week or day of one date value the task gives, in every row? (Redmine stores a time entry's ``tyear``, ``tmonth`` and ``tweek``.) The app computes it, so it isn't pinned.""" diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index ed25c8e436..5e546457f9 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1064,118 +1064,114 @@ def build_contract( # Rows beside the main record that hold a value (a note, a custom field, # an email address link), and list items (invoice lines). found = flavor == "found" - if True: - groups = [ - (p, g) - for p, g in plan.children.items() - if p in fields - or (not found and g.empty_when_null) - or (not found and plan.mode == "create" and set(g.co_owners) & fields) - ] - groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] - for p, g in groups: - name = f"rows:{g.table}" - sname = _alias(name) - if sname not in surfaces: - cols = _base_columns(b, g.table) - link = g.link - # The main record's values, seen from the child row. - for ip in identity_params: - loc = locations[ip] - up = Location( - (Hop("fk", anchor, link.column, link.ref_column, True),) - + loc.hops, - loc.columns, - loc.rule, - ) - expr = b.path("t", g.table, up) - if loc.rule in ("number", "date", "datetime", "minutes"): - expr = b.flag(expr, loc.rule, b.ph(ip)) - cols.append((f"main__{_alias(ip)}", expr)) - surfaces[sname] = Surface( - sname, settings.views.get(g.table, g.table), cols - ) - named_tables.add(g.table) - surf = surfaces[sname] - items = range(variant.lists[p]) if g.list_param else [None] - for i in items: - m: dict[str, Any] = {} - for ip in identity_params: - loc = locations[ip] - if loc.rule in ("number", "date", "datetime", "minutes"): - m[f"main__{_alias(ip)}"] = {"literal": "1"} - else: - m[f"main__{_alias(ip)}"] = _value_expr(ip, loc.rule) - for k, loc in g.params.items(): - pname = k if g.list_param is None else f"{p}.{i}.{k}" - if g.list_param is None and pname not in fields: - # Given another owner but not this value: empty. - for c in loc.columns if not loc.hops else [loc.hops[0].column]: - alias = f"empty__{_alias(c)}" - if alias not in {a for a, _ in surf.columns}: - surf.columns.append((alias, b.empty("t", g.table, c))) - m[alias] = {"literal": "1"} - continue - key = _place(b, surf, g.table, pname, loc) - if loc.rule in ("number", "date", "datetime", "minutes"): - m[key] = {"literal": "1"} - else: - m[key] = _value_expr(pname, loc.rule) - for col, role in sorted(g.roles.items()): - if role.role == "link" or found: - continue - _pin(b, surf, g.table, col, role, m, settings.actor_params) - effect = { - "kind": "record_written", - "probe": f"surface={sname}|a row beside the main record holds {p}", - "match": m, - "expected_count": 1, - } - if not found: - # Found: the row is already there, and nothing changes. - effect["count_new_only"] = True - effects.append(effect) - if plan.mode == "update": - target_fields.update(g.changed_columns) - # A new row a link row names (a new email address). - for p, ref in plan.added_refs.items(): - if p not in fields or found: - continue - effects.append( - _raw_effect( - b, - schema, - surfaces, - settings, - ref["table"], - {ref["column"]: _value_expr(p, ref["rule"])}, - 1, - {}, - f"the new {ref['table']} row holds {p}", + groups = [ + (p, g) + for p, g in plan.children.items() + if p in fields + or (not found and g.empty_when_null) + or (not found and plan.mode == "create" and set(g.co_owners) & fields) + ] + groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] + for p, g in groups: + name = f"rows:{g.table}" + sname = _alias(name) + if sname not in surfaces: + cols = _base_columns(b, g.table) + link = g.link + # The main record's values, seen from the child row. + for ip in identity_params: + loc = locations[ip] + up = Location( + (Hop("fk", anchor, link.column, link.ref_column, True),) + loc.hops, + loc.columns, + loc.rule, ) + expr = b.path("t", g.table, up) + if loc.rule in ("number", "date", "datetime", "minutes"): + expr = b.flag(expr, loc.rule, b.ph(ip)) + cols.append((f"main__{_alias(ip)}", expr)) + surfaces[sname] = Surface(sname, settings.views.get(g.table, g.table), cols) + named_tables.add(g.table) + surf = surfaces[sname] + items = range(variant.lists[p]) if g.list_param else [None] + for i in items: + m: dict[str, Any] = {} + for ip in identity_params: + loc = locations[ip] + if loc.rule in ("number", "date", "datetime", "minutes"): + m[f"main__{_alias(ip)}"] = {"literal": "1"} + else: + m[f"main__{_alias(ip)}"] = _value_expr(ip, loc.rule) + for k, loc in g.params.items(): + pname = k if g.list_param is None else f"{p}.{i}.{k}" + if g.list_param is None and pname not in fields: + # Given another owner but not this value: empty. + for c in loc.columns if not loc.hops else [loc.hops[0].column]: + alias = f"empty__{_alias(c)}" + if alias not in {a for a, _ in surf.columns}: + surf.columns.append((alias, b.empty("t", g.table, c))) + m[alias] = {"literal": "1"} + continue + key = _place(b, surf, g.table, pname, loc) + if loc.rule in ("number", "date", "datetime", "minutes"): + m[key] = {"literal": "1"} + else: + m[key] = _value_expr(pname, loc.rule) + for col, role in sorted(g.roles.items()): + if role.role == "link" or found: + continue + _pin(b, surf, g.table, col, role, m, settings.actor_params) + effect = { + "kind": "record_written", + "probe": f"surface={sname}|a row beside the main record holds {p}", + "match": m, + "expected_count": 1, + } + if not found: + # Found: the row is already there, and nothing changes. + effect["count_new_only"] = True + effects.append(effect) + if plan.mode == "update": + target_fields.update(g.changed_columns) + # A new row a link row names (a new email address). + for p, ref in plan.added_refs.items(): + if p not in fields or found: + continue + effects.append( + _raw_effect( + b, + schema, + surfaces, + settings, + ref["table"], + {ref["column"]: _value_expr(p, ref["rule"])}, + 1, + {}, + f"the new {ref['table']} row holds {p}", ) - for side in plan.sides: - if found or (side.unless is not None and side.unless in fields): - continue - m = {} - for col, role in sorted(side.roles.items()): - if role.role == "constant" and role.value is not None: - m[col] = {"literal": role.value} - elif role.role == "actor": - m[col] = {"param": role.detail["param"]} - effects.append( - _raw_effect( - b, - schema, - surfaces, - settings, - side.table, - m, - side.count, - side.roles, - f"{side.count} row(s) the app adds to {side.table}", - ) + ) + for side in plan.sides: + if found or (side.unless is not None and side.unless in fields): + continue + m = {} + for col, role in sorted(side.roles.items()): + if role.role == "constant" and role.value is not None: + m[col] = {"literal": role.value} + elif role.role == "actor": + m[col] = {"param": role.detail["param"]} + effects.append( + _raw_effect( + b, + schema, + surfaces, + settings, + side.table, + m, + side.count, + side.roles, + f"{side.count} row(s) the app adds to {side.table}", ) + ) verifier: dict[str, Any] = {"kind": "surfaces", "surfaces": {}} for s in surfaces.values(): @@ -2259,12 +2255,12 @@ def choose( def _exists(contract: Any, params: Mapping[str, Any]) -> bool: - """Does a record matching the found contract's main effect exist now?""" + """Does every effect of the found contract hold now (the record, and each + row beside it that holds a value)?""" from openadapt_flow.runtime.effects.effect import record_matches - from openadapt_flow.verify_run import _build_verifier - from openadapt_flow.runtime.effects.surfaces import surface_of + from openadapt_flow.verify_run import _build_verifier verifier = _build_verifier(contract.config, params) state = verifier.capture_pre_state() From da944eac529e10b2a1f578f1a42dc411fbf64c5c Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:20:46 -0400 Subject: [PATCH 17/94] WIP: copied columns need two rows --- packages/flow/openadapt_flow/generate/demo_analysis.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index c3c3b9bfdb..2ae42df47f 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -665,7 +665,9 @@ def _copied( every row? (An invoice line's unit price is its product's price.)""" col = t.column(name) - if col is None or col.kind not in ("number", "text"): + if col is None or col.kind not in ("number", "text") or len(rows) < 2: + # One row can't tell a copy from a coincidence (a user id equal to a + # project id). return None candidates: Optional[set[tuple[str, str, str, str]]] = None for demo, r in rows: @@ -675,7 +677,7 @@ def _copied( idx = _Index(schema, demo.after) here: set[tuple[str, str, str, str]] = set() for other in t.columns: - if other.name == name or r.get(other.name) is None: + if other.name == name or other.name in t.primary_key or r.get(other.name) is None: continue for target, ref, _declared in _fk_targets( schema, idx, t, other.name, str(r.get(other.name)) From 06ef14ab14f6cae94e2b540bfbb19b6bdfc50cfa Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:42:01 -0400 Subject: [PATCH 18/94] WIP: EspoCRM shapes: anchor scoring, tie-breaks, dispatch child rows, extra child places, views in subqueries --- .../generate_contracts/demo_espocrm.py | 8 +- .../results/redmine/QUESTIONS.md | 12 + .../results/redmine/answers.yaml | 1 + ...oject+subject+tracker.47c18f25.create.yaml | 674 +++++++++++ ...ject+project+spent_on.a2ffb74d.create.yaml | 578 +++++++++ ...ours+project+spent_on.c6f70a8e.create.yaml | 578 +++++++++ ...ject+project+spent_on.d1e5805e.create.yaml | 579 +++++++++ ...ours+project+spent_on.e07feee9.create.yaml | 579 +++++++++ ...roject+status+subject.4aa4d777.update.yaml | 590 +++++++++ .../results/redmine/manifest.yaml | 1052 +++++++++++++++++ .../results/redmine/report.json | 132 +++ .../openadapt_flow/generate/demo_analysis.py | 16 +- .../generate/verify_contract.py | 176 ++- 13 files changed, 4953 insertions(+), 22 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/report.json diff --git a/packages/flow/benchmark/generate_contracts/demo_espocrm.py b/packages/flow/benchmark/generate_contracts/demo_espocrm.py index 3b6c3ca9da..84feac8369 100644 --- a/packages/flow/benchmark/generate_contracts/demo_espocrm.py +++ b/packages/flow/benchmark/generate_contracts/demo_espocrm.py @@ -79,7 +79,9 @@ def api(method: str, path: str, payload: dict) -> None: cur.execute("SELECT COUNT(*) FROM contact WHERE BINARY first_name=%s AND BINARY last_name=%s AND deleted=0", (v["first_name"], v["last_name"])) # fmt: skip if cur.fetchone()[0]: - continue + # Already there: a create with values of our own. + v = {**v, "first_name": "Lena", "last_name": "Okafor", + "email": "lena.okafor@aurora-logistics.example"} body = {"firstName": v["first_name"], "lastName": v["last_name"], "accountId": account(v["account"], v["account_city"])} # fmt: skip if v.get("title"): @@ -96,7 +98,9 @@ def api(method: str, path: str, payload: dict) -> None: (v["name"],), ) if cur.fetchone()[0]: - continue + v = {**v, "name": "Aurora – Fleet Telematics Trial", "account": "Aurora Logistics", + "account_city": "Boston", "amount": 12500, "stage": "Proposal", + "close_date": "2027-02-15"} body = {"name": v["name"], "accountId": account(v["account"], v["account_city"]), "amount": v["amount"], "amountCurrency": v["currency"], "stage": v["stage"], "closeDate": v["close_date"]} # fmt: skip diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md new file mode 100644 index 0000000000..cef010825f --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md @@ -0,0 +1,12 @@ +# Questions for a person + +1. Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): custom_values (2 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change; on PostgreSQL they are not read at all): journal_details Columns that change on their own and are not compared: issues: lock_version, updated_on; user_preferences: others Columns of a new record that differ between demonstrations and are not checked: time_entries: tmonth, tweek, tyear Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. + - Answer key: `bookkeeping`. Proposed: `accept`. + - Answered: `"accept"` + +## Kinds + +- `create_issue` (create, main table `issues`): 26 contract(s). +- `update_issue` (update, main table `issues`): 63 contract(s). +- `log_time` (create, main table `time_entries`): 4 contract(s). +- `ensure_issue` (create, main table `issues`): 2 contract(s). diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml new file mode 100644 index 0000000000..402fc2163f --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml @@ -0,0 +1 @@ +bookkeeping: accept diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml new file mode 100644 index 0000000000..9ffc2e8b23 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml @@ -0,0 +1,674 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_issue, create, fields assignee, customer_ref, due_date, environment, priority, project, subject, tracker. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."firstname" + AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) FROM "users" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" + AS TEXT)) AS "p__assignee", CASE WHEN t."due_date" = CAST(%(v0)s AS date) THEN '1' ELSE '0' END + AS "p__due_date", (SELECT CAST(a1."name" AS TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS + TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CAST(a1."name" AS TEXT) FROM + "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT + CAST(a1."name" AS TEXT) FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" AS + TEXT)) AS "p__tracker", CASE WHEN t."category_id" IS NULL THEN '1' ELSE '0' END AS "pin__category_id", + CASE WHEN t."closed_on" IS NULL THEN '1' ELSE '0' END AS "pin__closed_on", CASE WHEN CAST(t."done_ratio" + AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__done_ratio", CASE WHEN t."estimated_hours" + IS NULL THEN '1' ELSE '0' END AS "pin__estimated_hours", CASE WHEN t."fixed_version_id" IS NULL + THEN '1' ELSE '0' END AS "pin__fixed_version_id", CASE WHEN CAST(CAST(t."is_private" AS integer) + AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__is_private", CASE WHEN CAST(t."lft" + AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__lft", CASE WHEN CAST(t."lock_version" + AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__lock_version", CASE WHEN t."parent_id" + IS NULL THEN '1' ELSE '0' END AS "pin__parent_id", CASE WHEN CAST(t."rgt" AS numeric) = CAST('2' + AS numeric) THEN '1' ELSE '0' END AS "pin__rgt", CASE WHEN t."start_date" IS NULL THEN '1' ELSE + '0' END AS "pin__start_date", CASE WHEN CAST(t."status_id" AS numeric) = CAST('1' AS numeric) + THEN '1' ELSE '0' END AS "pin__status_id", CASE WHEN t."description" IS NULL OR t."description" + = '' THEN '1' ELSE '0' END AS "empty__description" FROM "issues" t + sql_query_params: + v0: &id002 + param: due_date + rows_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CAST(a2."firstname" AS TEXT) || ' ' || CAST(a2."lastname" + AS TEXT) FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" AS TEXT)) FROM + "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", CASE WHEN (SELECT a1."due_date" + FROM "issues" a1 WHERE a1."id" = t."customized_id") = CAST(%(v0)s AS date) THEN '1' ELSE '0' END + AS "main__due_date", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "enumerations" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."priority_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") + AS "main__priority", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") + AS "main__project", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") + AS "main__subject", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "trackers" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."tracker_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") + AS "main__tracker", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('1' AS numeric) THEN + '1' ELSE '0' END AS "pin__custom_field_id", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('2' + AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id__47c66b" FROM "custom_values" t + sql_query_params: + v0: *id002 + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id" FROM "journals" t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_time_entries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__assignee: + param: assignee + p__due_date: + literal: '1' + p__priority: + param: priority + p__project: + param: project + subject: + param: subject + p__tracker: + param: tracker + author_id: + param: bot_user_id + pin__category_id: + literal: '1' + pin__closed_on: + literal: '1' + pin__done_ratio: + literal: '1' + pin__estimated_hours: + literal: '1' + pin__fixed_version_id: + literal: '1' + pin__is_private: + literal: '1' + pin__lft: + literal: '1' + pin__lock_version: + literal: '1' + pin__parent_id: + literal: '1' + pin__rgt: + literal: '1' + pin__start_date: + literal: '1' + pin__status_id: + literal: '1' + empty__description: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_custom_values|a row beside the main record holds customer_ref + match: + main__assignee: + param: assignee + main__due_date: + literal: '1' + main__priority: + param: priority + main__project: + param: project + main__subject: + param: subject + main__tracker: + param: tracker + value: + param: customer_ref + pin__custom_field_id: + literal: '1' + customized_type: + literal: Issue + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_custom_values|a row beside the main record holds environment + match: + main__assignee: + param: assignee + main__due_date: + literal: '1' + main__priority: + param: priority + main__project: + param: project + main__subject: + param: subject + main__tracker: + param: tracker + value: + param: environment + pin__custom_field_id__47c66b: + literal: '1' + customized_type: + literal: Issue + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: [] diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml new file mode 100644 index 0000000000..0de24b0cd5 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml @@ -0,0 +1,578 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, comments, hours, issue_subject, project, spent_on. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS + TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", + CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", + (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."issue_id" + AS TEXT)) AS "p__issue_subject", (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s + AS date) THEN '1' ELSE '0' END AS "p__spent_on" FROM "time_entries" t + sql_query_params: + v0: + param: hours + v1: + param: spent_on + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id" FROM "journals" t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__activity: + param: activity + comments: + param: comments + p__hours: + literal: '1' + p__issue_subject: + param: issue_subject + p__project: + param: project + p__spent_on: + literal: '1' + author_id: + param: bot_user_id + user_id: + param: bot_user_id + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: [] diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml new file mode 100644 index 0000000000..edbd5dd64f --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml @@ -0,0 +1,578 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, comments, hours, project, spent_on. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS + TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", + CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", + (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" + AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END + AS "p__spent_on", CASE WHEN t."issue_id" IS NULL THEN '1' ELSE '0' END AS "empty__issue_id" FROM + "time_entries" t + sql_query_params: + v0: + param: hours + v1: + param: spent_on + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id" FROM "journals" t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__activity: + param: activity + comments: + param: comments + p__hours: + literal: '1' + p__project: + param: project + p__spent_on: + literal: '1' + author_id: + param: bot_user_id + user_id: + param: bot_user_id + empty__issue_id: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: [] diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml new file mode 100644 index 0000000000..e1d5b82da8 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml @@ -0,0 +1,579 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, hours, issue_subject, project, spent_on. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS + TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", + CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", + (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."issue_id" + AS TEXT)) AS "p__issue_subject", (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s + AS date) THEN '1' ELSE '0' END AS "p__spent_on", CASE WHEN t."comments" IS NULL OR t."comments" + = '' THEN '1' ELSE '0' END AS "empty__comments" FROM "time_entries" t + sql_query_params: + v0: + param: hours + v1: + param: spent_on + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id" FROM "journals" t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__activity: + param: activity + p__hours: + literal: '1' + p__issue_subject: + param: issue_subject + p__project: + param: project + p__spent_on: + literal: '1' + author_id: + param: bot_user_id + user_id: + param: bot_user_id + empty__comments: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: [] diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml new file mode 100644 index 0000000000..80afe1cb97 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml @@ -0,0 +1,579 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, hours, project, spent_on. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS + TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", + CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", + (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" + AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END + AS "p__spent_on", CASE WHEN t."comments" IS NULL OR t."comments" = '' THEN '1' ELSE '0' END AS + "empty__comments", CASE WHEN t."issue_id" IS NULL THEN '1' ELSE '0' END AS "empty__issue_id" FROM + "time_entries" t + sql_query_params: + v0: + param: hours + v1: + param: spent_on + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id" FROM "journals" t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__activity: + param: activity + p__hours: + literal: '1' + p__project: + param: project + p__spent_on: + literal: '1' + author_id: + param: bot_user_id + user_id: + param: bot_user_id + empty__comments: + literal: '1' + empty__issue_id: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: [] diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml new file mode 100644 index 0000000000..3c41dbe83f --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml @@ -0,0 +1,590 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_issue, update, fields notes, project, status, subject. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."name" AS + TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", + (SELECT CAST(a1."name" AS TEXT) FROM "issue_statuses" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."status_id" + AS TEXT)) AS "p__status" FROM "issues" t + rows_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE + CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__project", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "issue_statuses" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."status_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__status", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__subject", CASE WHEN CAST(CAST(t."private_notes" AS integer) AS numeric) = CAST('0' AS + numeric) THEN '1' ELSE '0' END AS "pin__private_notes", CASE WHEN t."updated_by_id" IS NULL THEN + '1' ELSE '0' END AS "pin__updated_by_id", CASE WHEN CAST(t."user_id" AS numeric) = CAST('5' AS + numeric) THEN '1' ELSE '0' END AS "pin__user_id" FROM "journals" t + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_time_entries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__project: + param: project + p__status: + param: status + subject: + param: subject + expected_count: 1 +- kind: record_written + probe: surface=rows_journals|a row beside the main record holds notes + match: + main__project: + param: project + main__status: + param: status + main__subject: + param: subject + notes: + param: notes + journalized_type: + literal: Issue + pin__private_notes: + literal: '1' + pin__updated_by_id: + literal: '1' + pin__user_id: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: + - p__status + - status_id diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml new file mode 100644 index 0000000000..aca034aa1e --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml @@ -0,0 +1,1052 @@ +schema_version: openadapt.contract-manifest/v1 +dialect: postgresql +database: redmine +actor_params: +- bot_user_id +questions_open: [] +kinds: + create_issue: + mode: create + main_table: issues + parameters: + - project + - tracker + - subject + - priority + - assignee + - due_date + - description + - customer_ref + - environment + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - assignee + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+due_date+priority+project+subject+tracker.cb8524d8.create.yaml + found: contracts/create_issue/assignee+due_date+priority+project+subject+tracker.cb8524d8.found.yaml + - fields: + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.create.yaml + found: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.found.yaml + - fields: + - assignee + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.create.yaml + found: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.found.yaml + - fields: + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/priority+project+subject+tracker.a0dc8790.create.yaml + found: contracts/create_issue/priority+project+subject+tracker.a0dc8790.found.yaml + - fields: + - assignee + - customer_ref + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.create.yaml + found: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.found.yaml + - fields: + - customer_ref + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+environment+priority+project+subject+tracker.e94e6033.create.yaml + found: contracts/create_issue/customer_ref+environment+priority+project+subject+tracker.e94e6033.found.yaml + - fields: + - assignee + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+environment+priority+project+subject+tracker.b1832d09.create.yaml + found: contracts/create_issue/assignee+environment+priority+project+subject+tracker.b1832d09.found.yaml + - fields: + - assignee + - customer_ref + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+priority+project+subject+tracker.88f7523d.create.yaml + found: contracts/create_issue/assignee+customer_ref+priority+project+subject+tracker.88f7523d.found.yaml + - fields: + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/environment+priority+project+subject+tracker.99d89352.create.yaml + found: contracts/create_issue/environment+priority+project+subject+tracker.99d89352.found.yaml + - fields: + - customer_ref + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+priority+project+subject+tracker.322376d5.create.yaml + found: contracts/create_issue/customer_ref+priority+project+subject+tracker.322376d5.found.yaml + - fields: + - assignee + - customer_ref + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+due_date+priority+project+subject+tracker.f7bf10f1.create.yaml + found: contracts/create_issue/assignee+customer_ref+due_date+priority+project+subject+tracker.f7bf10f1.found.yaml + - fields: + - assignee + - description + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+description+due_date+priority+project+subject+tracker.3045a6ca.create.yaml + found: contracts/create_issue/assignee+description+due_date+priority+project+subject+tracker.3045a6ca.found.yaml + - fields: + - assignee + - due_date + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+due_date+environment+priority+project+subject+tracker.9f2faa3a.create.yaml + found: contracts/create_issue/assignee+due_date+environment+priority+project+subject+tracker.9f2faa3a.found.yaml + - fields: + - customer_ref + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.create.yaml + found: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.found.yaml + - fields: + - description + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.create.yaml + found: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.found.yaml + - fields: + - due_date + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.create.yaml + found: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.found.yaml + - fields: + - assignee + - description + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.create.yaml + found: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.found.yaml + - fields: + - description + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.create.yaml + found: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.found.yaml + - fields: + - assignee + - customer_ref + - description + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml + found: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml + - fields: + - assignee + - customer_ref + - due_date + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml + found: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.found.yaml + - fields: + - customer_ref + - description + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+description+environment+priority+project+subject+tracker.8bd3a0b3.create.yaml + found: contracts/create_issue/customer_ref+description+environment+priority+project+subject+tracker.8bd3a0b3.found.yaml + - fields: + - customer_ref + - due_date + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+due_date+environment+priority+project+subject+tracker.018e5a55.create.yaml + found: contracts/create_issue/customer_ref+due_date+environment+priority+project+subject+tracker.018e5a55.found.yaml + - fields: + - assignee + - description + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+description+environment+priority+project+subject+tracker.1563d2ca.create.yaml + found: contracts/create_issue/assignee+description+environment+priority+project+subject+tracker.1563d2ca.found.yaml + - fields: + - assignee + - customer_ref + - description + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+description+priority+project+subject+tracker.4d3aeb69.create.yaml + found: contracts/create_issue/assignee+customer_ref+description+priority+project+subject+tracker.4d3aeb69.found.yaml + - fields: + - description + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/description+environment+priority+project+subject+tracker.4203fe30.create.yaml + found: contracts/create_issue/description+environment+priority+project+subject+tracker.4203fe30.found.yaml + - fields: + - customer_ref + - description + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+description+priority+project+subject+tracker.c483a119.create.yaml + found: contracts/create_issue/customer_ref+description+priority+project+subject+tracker.c483a119.found.yaml + update_issue: + mode: update + main_table: issues + parameters: + - project + - subject + - status + - assignee + - priority + - due_date + - done_ratio + - notes + identity: + - project + - subject + lists: [] + dispatch: null + variants: + - fields: + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/project+status+subject.f5cf2619.update.yaml + found: contracts/update_issue/project+status+subject.f5cf2619.found.yaml + - fields: + - assignee + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+project+subject.4e643112.update.yaml + found: contracts/update_issue/assignee+project+subject.4e643112.found.yaml + - fields: + - due_date + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+project+subject.4f8546da.update.yaml + found: contracts/update_issue/due_date+project+subject.4f8546da.found.yaml + - fields: + - done_ratio + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+project+subject.5f3c3fa0.update.yaml + found: contracts/update_issue/done_ratio+project+subject.5f3c3fa0.found.yaml + - fields: + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/notes+project+subject.05b2ea74.update.yaml + found: contracts/update_issue/notes+project+subject.05b2ea74.found.yaml + - fields: + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/priority+project+subject.a57fbe91.update.yaml + found: contracts/update_issue/priority+project+subject.a57fbe91.found.yaml + - fields: + - assignee + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+project+status+subject.a05db09f.update.yaml + found: contracts/update_issue/assignee+project+status+subject.a05db09f.found.yaml + - fields: + - due_date + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+project+status+subject.0af96bde.update.yaml + found: contracts/update_issue/due_date+project+status+subject.0af96bde.found.yaml + - fields: + - done_ratio + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+project+status+subject.474ffed4.update.yaml + found: contracts/update_issue/done_ratio+project+status+subject.474ffed4.found.yaml + - fields: + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml + found: contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml + - fields: + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/priority+project+status+subject.bbe05441.update.yaml + found: contracts/update_issue/priority+project+status+subject.bbe05441.found.yaml + - fields: + - assignee + - due_date + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+project+subject.159f21e0.update.yaml + found: contracts/update_issue/assignee+due_date+project+subject.159f21e0.found.yaml + - fields: + - assignee + - done_ratio + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+project+subject.5df67a01.update.yaml + found: contracts/update_issue/assignee+done_ratio+project+subject.5df67a01.found.yaml + - fields: + - assignee + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+notes+project+subject.bbff0247.update.yaml + found: contracts/update_issue/assignee+notes+project+subject.bbff0247.found.yaml + - fields: + - assignee + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+priority+project+subject.b5e14879.update.yaml + found: contracts/update_issue/assignee+priority+project+subject.b5e14879.found.yaml + - fields: + - done_ratio + - due_date + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+project+subject.9d543b40.update.yaml + found: contracts/update_issue/done_ratio+due_date+project+subject.9d543b40.found.yaml + - fields: + - due_date + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+notes+project+subject.6f69bb62.update.yaml + found: contracts/update_issue/due_date+notes+project+subject.6f69bb62.found.yaml + - fields: + - due_date + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+priority+project+subject.119bebbc.update.yaml + found: contracts/update_issue/due_date+priority+project+subject.119bebbc.found.yaml + - fields: + - done_ratio + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+notes+project+subject.42cc7615.update.yaml + found: contracts/update_issue/done_ratio+notes+project+subject.42cc7615.found.yaml + - fields: + - done_ratio + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+priority+project+subject.3f024f0d.update.yaml + found: contracts/update_issue/done_ratio+priority+project+subject.3f024f0d.found.yaml + - fields: + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/notes+priority+project+subject.8682b0ac.update.yaml + found: contracts/update_issue/notes+priority+project+subject.8682b0ac.found.yaml + - fields: + - assignee + - due_date + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+project+status+subject.c3a69cb4.update.yaml + found: contracts/update_issue/assignee+due_date+project+status+subject.c3a69cb4.found.yaml + - fields: + - assignee + - done_ratio + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+project+status+subject.54276404.update.yaml + found: contracts/update_issue/assignee+done_ratio+project+status+subject.54276404.found.yaml + - fields: + - assignee + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+notes+project+status+subject.c7d221b1.update.yaml + found: contracts/update_issue/assignee+notes+project+status+subject.c7d221b1.found.yaml + - fields: + - assignee + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+priority+project+status+subject.bc85aafd.update.yaml + found: contracts/update_issue/assignee+priority+project+status+subject.bc85aafd.found.yaml + - fields: + - done_ratio + - due_date + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+project+status+subject.46328550.update.yaml + found: contracts/update_issue/done_ratio+due_date+project+status+subject.46328550.found.yaml + - fields: + - due_date + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+notes+project+status+subject.73fff321.update.yaml + found: contracts/update_issue/due_date+notes+project+status+subject.73fff321.found.yaml + - fields: + - due_date + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+priority+project+status+subject.6461d8ca.update.yaml + found: contracts/update_issue/due_date+priority+project+status+subject.6461d8ca.found.yaml + - fields: + - done_ratio + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+notes+project+status+subject.aec2705d.update.yaml + found: contracts/update_issue/done_ratio+notes+project+status+subject.aec2705d.found.yaml + - fields: + - done_ratio + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+priority+project+status+subject.5b1b1f40.update.yaml + found: contracts/update_issue/done_ratio+priority+project+status+subject.5b1b1f40.found.yaml + - fields: + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/notes+priority+project+status+subject.a5a6c598.update.yaml + found: contracts/update_issue/notes+priority+project+status+subject.a5a6c598.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+project+subject.e06f0a37.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+project+subject.e06f0a37.found.yaml + - fields: + - assignee + - due_date + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+notes+project+subject.177404ca.update.yaml + found: contracts/update_issue/assignee+due_date+notes+project+subject.177404ca.found.yaml + - fields: + - assignee + - due_date + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+priority+project+subject.bed7fd8a.update.yaml + found: contracts/update_issue/assignee+due_date+priority+project+subject.bed7fd8a.found.yaml + - fields: + - assignee + - done_ratio + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+notes+project+subject.7054e59a.update.yaml + found: contracts/update_issue/assignee+done_ratio+notes+project+subject.7054e59a.found.yaml + - fields: + - assignee + - done_ratio + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+priority+project+subject.887e7284.update.yaml + found: contracts/update_issue/assignee+done_ratio+priority+project+subject.887e7284.found.yaml + - fields: + - assignee + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+notes+priority+project+subject.c8e8610c.update.yaml + found: contracts/update_issue/assignee+notes+priority+project+subject.c8e8610c.found.yaml + - fields: + - done_ratio + - due_date + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+notes+project+subject.036ba8cb.update.yaml + found: contracts/update_issue/done_ratio+due_date+notes+project+subject.036ba8cb.found.yaml + - fields: + - done_ratio + - due_date + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+priority+project+subject.530b95ee.update.yaml + found: contracts/update_issue/done_ratio+due_date+priority+project+subject.530b95ee.found.yaml + - fields: + - due_date + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+notes+priority+project+subject.185b2051.update.yaml + found: contracts/update_issue/due_date+notes+priority+project+subject.185b2051.found.yaml + - fields: + - done_ratio + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+notes+priority+project+subject.3b999ec2.update.yaml + found: contracts/update_issue/done_ratio+notes+priority+project+subject.3b999ec2.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+project+status+subject.80ad363e.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+project+status+subject.80ad363e.found.yaml + - fields: + - assignee + - due_date + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+notes+project+status+subject.07561ad0.update.yaml + found: contracts/update_issue/assignee+due_date+notes+project+status+subject.07561ad0.found.yaml + - fields: + - assignee + - due_date + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+priority+project+status+subject.9b95d736.update.yaml + found: contracts/update_issue/assignee+due_date+priority+project+status+subject.9b95d736.found.yaml + - fields: + - assignee + - done_ratio + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+notes+project+status+subject.b2c7c267.update.yaml + found: contracts/update_issue/assignee+done_ratio+notes+project+status+subject.b2c7c267.found.yaml + - fields: + - assignee + - done_ratio + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+priority+project+status+subject.276a5a30.update.yaml + found: contracts/update_issue/assignee+done_ratio+priority+project+status+subject.276a5a30.found.yaml + - fields: + - assignee + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+notes+priority+project+status+subject.d1cc724b.update.yaml + found: contracts/update_issue/assignee+notes+priority+project+status+subject.d1cc724b.found.yaml + - fields: + - done_ratio + - due_date + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+notes+project+status+subject.f2171a53.update.yaml + found: contracts/update_issue/done_ratio+due_date+notes+project+status+subject.f2171a53.found.yaml + - fields: + - done_ratio + - due_date + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+priority+project+status+subject.14f98761.update.yaml + found: contracts/update_issue/done_ratio+due_date+priority+project+status+subject.14f98761.found.yaml + - fields: + - due_date + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+notes+priority+project+status+subject.29f05bb8.update.yaml + found: contracts/update_issue/due_date+notes+priority+project+status+subject.29f05bb8.found.yaml + - fields: + - done_ratio + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+notes+priority+project+status+subject.eec8202f.update.yaml + found: contracts/update_issue/done_ratio+notes+priority+project+status+subject.eec8202f.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+notes+project+subject.7ff66a52.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+notes+project+subject.7ff66a52.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+priority+project+subject.6eb0e43c.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+priority+project+subject.6eb0e43c.found.yaml + - fields: + - assignee + - due_date + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+notes+priority+project+subject.88ebb1f9.update.yaml + found: contracts/update_issue/assignee+due_date+notes+priority+project+subject.88ebb1f9.found.yaml + - fields: + - assignee + - done_ratio + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+notes+priority+project+subject.4153e619.update.yaml + found: contracts/update_issue/assignee+done_ratio+notes+priority+project+subject.4153e619.found.yaml + - fields: + - done_ratio + - due_date + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+notes+priority+project+subject.8da31395.update.yaml + found: contracts/update_issue/done_ratio+due_date+notes+priority+project+subject.8da31395.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+notes+project+status+subject.02a8e039.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+notes+project+status+subject.02a8e039.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+priority+project+status+subject.113ce920.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+priority+project+status+subject.113ce920.found.yaml + - fields: + - assignee + - due_date + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+notes+priority+project+status+subject.6ea08e8b.update.yaml + found: contracts/update_issue/assignee+due_date+notes+priority+project+status+subject.6ea08e8b.found.yaml + - fields: + - assignee + - done_ratio + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+notes+priority+project+status+subject.31317326.update.yaml + found: contracts/update_issue/assignee+done_ratio+notes+priority+project+status+subject.31317326.found.yaml + - fields: + - done_ratio + - due_date + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+notes+priority+project+status+subject.3cf03089.update.yaml + found: contracts/update_issue/done_ratio+due_date+notes+priority+project+status+subject.3cf03089.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+subject.514a3f86.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+subject.514a3f86.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+status+subject.2d1635fd.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+status+subject.2d1635fd.found.yaml + log_time: + mode: create + main_table: time_entries + parameters: + - project + - issue_subject + - hours + - spent_on + - activity + - comments + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - activity + - comments + - hours + - issue_subject + - project + - spent_on + lists: {} + dispatch_value: null + create: contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml + found: contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml + - fields: + - activity + - hours + - issue_subject + - project + - spent_on + lists: {} + dispatch_value: null + create: contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml + found: contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.found.yaml + - fields: + - activity + - comments + - hours + - project + - spent_on + lists: {} + dispatch_value: null + create: contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml + found: contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.found.yaml + - fields: + - activity + - hours + - project + - spent_on + lists: {} + dispatch_value: null + create: contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml + found: contracts/log_time/activity+hours+project+spent_on.e07feee9.found.yaml + ensure_issue: + mode: create + main_table: issues + parameters: + - project + - tracker + - subject + - priority + - assignee + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - assignee + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/ensure_issue/assignee+priority+project+subject+tracker.d16f0df9.create.yaml + found: contracts/ensure_issue/assignee+priority+project+subject+tracker.d16f0df9.found.yaml + - fields: + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/ensure_issue/priority+project+subject+tracker.a0dc8790.create.yaml + found: contracts/ensure_issue/priority+project+subject+tracker.a0dc8790.found.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/report.json b/packages/flow/benchmark/generate_contracts/results/redmine/report.json new file mode 100644 index 0000000000..76309b9659 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/report.json @@ -0,0 +1,132 @@ +{ + "applied": [], + "dialect": "postgresql", + "tables": 58, + "questions": [ + { + "number": 1, + "key": "bookkeeping", + "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): custom_values (2 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change; on PostgreSQL they are not read at all): journal_details Columns that change on their own and are not compared: issues: lock_version, updated_on; user_preferences: others Columns of a new record that differ between demonstrations and are not checked: time_entries: tmonth, tweek, tyear Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", + "proposal": "accept", + "answered": true + } + ], + "kinds": { + "create_issue": { + "mode": "create", + "main_table": "issues", + "locations": { + "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", + "customer_ref": "custom_values (whose customized_id is the issues key).value (exact)", + "description": "issues.description (exact)", + "due_date": "issues.due_date (date)", + "environment": "custom_values (whose customized_id is the issues key).value (exact)", + "priority": "issues.priority_id -> enumerations.name (exact)", + "project": "issues.project_id -> projects.name (exact)", + "subject": "issues.subject (exact)", + "tracker": "issues.tracker_id -> trackers.name (exact)" + }, + "placed_by_name": [ + "description" + ], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": { + "customer_ref": "custom_values", + "environment": "custom_values" + }, + "lists": {}, + "side_rows": [], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 26, + "rejected": [] + }, + "update_issue": { + "mode": "update", + "main_table": "issues", + "locations": { + "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", + "done_ratio": "issues.done_ratio (number)", + "due_date": "issues.due_date (date)", + "notes": "journals (whose journalized_id is the issues key).notes (exact)", + "priority": "issues.priority_id -> enumerations.name (exact)", + "project": "issues.project_id -> projects.name (exact)", + "status": "issues.status_id -> issue_statuses.name (exact)", + "subject": "issues.subject (exact)" + }, + "placed_by_name": [ + "done_ratio" + ], + "placed_from_another_kind": [ + "due_date", + "priority" + ], + "demonstrations": 2, + "children": { + "notes": "journals" + }, + "lists": {}, + "side_rows": [], + "logs": [ + "journal_details" + ], + "refused": [], + "unresolved": {}, + "emitted": 63, + "rejected": [] + }, + "log_time": { + "mode": "create", + "main_table": "time_entries", + "locations": { + "activity": "time_entries.activity_id -> enumerations.name (exact)", + "comments": "time_entries.comments (exact)", + "hours": "time_entries.hours (number)", + "issue_subject": "time_entries.issue_id -> issues.subject (exact)", + "project": "time_entries.project_id -> projects.name (exact)", + "spent_on": "time_entries.spent_on (date)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": {}, + "lists": {}, + "side_rows": [], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 4, + "rejected": [] + }, + "ensure_issue": { + "mode": "create", + "main_table": "issues", + "locations": { + "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", + "priority": "issues.priority_id -> enumerations.name (exact)", + "project": "issues.project_id -> projects.name (exact)", + "subject": "issues.subject (exact)", + "tracker": "issues.tracker_id -> trackers.name (exact)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": {}, + "lists": {}, + "side_rows": [ + { + "table": "custom_values", + "count": 2, + "unless": null + } + ], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 2, + "rejected": [] + } + } +} \ No newline at end of file diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 2ae42df47f..7eee2fcf2f 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -485,6 +485,16 @@ def pick_anchor( ) else: hit = locate(schema, demo, ch, v, max_hops=1) + # Only the row's own columns, and keys named after the table they + # point to: a row the write added beside the main record (an + # issue's journal) reaches the record's values only through a + # link back, and a key equal to some row's id by chance proves + # nothing. + hit = [ + loc + for loc in hit + if all(h.kind == "fk" and _hop_cost(h) <= 1 for h in loc.hops) + ] score += 1 if hit else 0 direct += 1 if any(not loc.hops for loc in hit) else 0 if (score, direct) > (best_score, best_direct): @@ -677,7 +687,11 @@ def _copied( idx = _Index(schema, demo.after) here: set[tuple[str, str, str, str]] = set() for other in t.columns: - if other.name == name or other.name in t.primary_key or r.get(other.name) is None: + if ( + other.name == name + or other.name in t.primary_key + or r.get(other.name) is None + ): continue for target, ref, _declared in _fk_targets( schema, idx, t, other.name, str(r.get(other.name)) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 5e546457f9..90240c1cea 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -195,6 +195,9 @@ class ChildGroup: #: The app adds this row with an empty value when the task leaves the #: parameter out (Redmine's custom field values): expected then too. empty_when_null: bool = False + #: The parameter whose value the rows hold (``children`` may hold a + #: second group for it: a call's lead is its parent and an attendee). + param: str = "" @dataclass @@ -234,10 +237,17 @@ class KindPlan: unresolved: dict[str, list[Location]] = field(default_factory=dict) refused: list[str] = field(default_factory=list) removed: dict[str, int] = field(default_factory=dict) + #: dispatch value -> group name -> rows that hold the value param. + dispatch_children: dict[str, dict[str, "ChildGroup"]] = field(default_factory=dict) + #: unresolved key -> (parameter, dispatch value or None). + unresolved_meta: dict[str, tuple[str, Optional[str]]] = field(default_factory=dict) #: Parameters placed by name, not by a demonstration. by_name: list[str] = field(default_factory=list) #: Parameters placed where another kind's demonstrations found them. shared: list[str] = field(default_factory=list) + #: More places a value is in: rows the write added beside the record + #: (a call's lead is its parent and also an attendee row). + extra: dict[str, list[Location]] = field(default_factory=dict) def _scalars(values: Mapping[str, Any]) -> dict[str, Any]: @@ -340,7 +350,7 @@ def analyze( for p in scalars: if p == plan.dispatch: continue - if plan.dispatch is not None: + if plan.dispatch is not None and "identity" not in roles_of[p]: by_value: dict[str, list[list[Location]]] = {} for d, locs in zip( [d for d in plan.demos if d.values.get(p) is not None], found[p] @@ -349,16 +359,31 @@ def analyze( if len(by_value) > 1: ok = True for dv, per in by_value.items(): - loc, tied = best(_intersect(per)) + loc, tied = _pick(p, _intersect(per)) if loc is None: - plan.unresolved[f"{p} (when {plan.dispatch} is {dv})"] = tied + key = f"{p} (when {plan.dispatch} is {dv})" + plan.unresolved[key] = tied + plan.unresolved_meta[key] = (p, dv) ok = False else: plan.dispatch_locations.setdefault(dv, {})[p] = loc if ok: plan.changes.append(p) continue - loc, tied = best(_intersect(found[p])) + all_found = _intersect(found[p]) + loc, tied = _pick(p, all_found) + if loc is not None: + plan.extra[p] = [ + x + for x in all_found + if x.hops + and x.hops[0].kind == "child" + and x != loc + # in another table than the main place, through keys named + # after their tables (a call's lead attendee row) + and (not loc.hops or x.hops[0].table != loc.hops[0].table) + and all(_hop_cost(h) <= 1 for h in x.hops[1:]) + ] if loc is None: if not found[p]: named = _by_name(schema, plan, p) @@ -369,6 +394,7 @@ def analyze( plan.changes.append(p) continue # never given: no contract uses it plan.unresolved[p] = tied + plan.unresolved_meta[p] = (p, None) continue plan.locations[p] = loc if plan.mode == "update": @@ -453,6 +479,64 @@ def _share_locations( break +def _pick( + param: str, found: Sequence[Location] +) -> tuple[Optional[Location], list[Location]]: + """The best location; among tied ones, the one whose column is named + like the parameter (``stage``, not ``last_stage``), if exactly one is.""" + + loc, tied = best(found) + if loc is not None or not tied: + return loc, tied + name = re.sub(r"^(new|set)_", "", param) + named = [t for t in tied if t.columns and t.columns[-1] == name] + if len(named) == 1: + return named[0], [] + return None, tied + + +def _resolve(plan: KindPlan, key: str, loc: Location) -> None: + """Place an unresolved value (answered, or settled by a tie-break).""" + + param, dv = plan.unresolved_meta.get(key, (key, None)) + plan.unresolved.pop(key, None) + if dv is not None: + plan.dispatch_locations.setdefault(dv, {})[param] = loc + if param not in plan.changes: + plan.changes.append(param) + return + plan.locations[param] = loc + if plan.mode == "update" and param not in plan.identity + plan.changes: + plan.changes.append(param) + + +def _tail(plan: KindPlan, loc: Location) -> tuple[str, tuple[str, ...]]: + return (loc.hops[-1].table if loc.hops else plan.anchor, loc.columns) + + +def _break_ties_across(plans: Sequence[KindPlan]) -> None: + """A value tied between columns of one table (an email's ``name`` and + ``lower``) goes where another kind's demonstrations put the same kind + of value, if exactly one tied place is one of those.""" + + for plan in plans: + for key, tied in list(plan.unresolved.items()): + if not tied: + continue + seen = { + _tail(o, loc) + for o in plans + if o is not plan + for loc in [ + *o.locations.values(), + *[x for d in o.dispatch_locations.values() for x in d.values()], + ] + } + hits = [t for t in tied if _tail(plan, t) in seen] + if len(hits) == 1: + _resolve(plan, key, hits[0]) + + def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: """An update parameter no demonstration gives (``new_zip``), placed at the main table's column of the same name after a leading ``new_`` or @@ -477,17 +561,33 @@ def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: return Location((), (name,), rule) -def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: +def _children( + schema: Schema, plan: KindPlan, lists: Sequence[str], only_missing: bool = False +) -> None: """Group child-row locations, and find each list parameter's rows.""" - for p, loc in list(plan.locations.items()): + todo: list[tuple[str, str, Location, Optional[str]]] = [] + for p, loc in plan.locations.items(): + todo.append((p, p, loc, None)) + for i, x in enumerate(plan.extra.get(p, [])): + todo.append((f"{p}#{i + 2}", p, x, None)) + for dv, locs in plan.dispatch_locations.items(): + for p, loc in locs.items(): + todo.append((f"{p}@{dv}", p, loc, dv)) + for gname, p, loc, dv in todo: if not loc.hops or loc.hops[0].kind != "child": continue + if only_missing and ( + gname in plan.children or gname in plan.dispatch_children.get(dv or "", {}) + ): + continue rel = Location(loc.hops[1:], loc.columns, loc.rule) rows = [] for d, a in zip(plan.demos, plan.anchors): if d.values.get(p) is None: continue + if dv is not None and str(d.values.get(plan.dispatch)) != dv: + continue for ch in d.diff.of(loc.hops[0].table, "added"): if ch.after is None: continue @@ -522,14 +622,19 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: ): co.append(q) explained |= {h[0].hops[0].column for h in hits} - plan.children[p] = ChildGroup( + group = ChildGroup( loc.hops[0].table, loc.hops[0], {p: rel}, column_roles(schema, loc.hops[0].table, rows, explained, {}), 1, co_owners=co, + param=p, ) + if dv is None: + plan.children[gname] = group + else: + plan.dispatch_children.setdefault(dv, {})[gname] = group for p in lists: per_key: dict[str, list[list[Location]]] = {} table = link = None @@ -603,10 +708,26 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: ) +def _all_groups(plan: KindPlan, dv: Optional[str] = None) -> list[ChildGroup]: + """Every child group; with ``dv``, the plain ones and that dispatch + value's own.""" + + out = list(plan.children.values()) + for value, groups in plan.dispatch_children.items(): + if dv is None or value == dv: + out.extend(groups.values()) + return out + + +def _demo_groups(plan: KindPlan, d: Demo) -> list[ChildGroup]: + dv = str(d.values.get(plan.dispatch)) if plan.dispatch else None + return _all_groups(plan, dv) if dv is not None else list(plan.children.values()) + + def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> None: """Classify every other row the demonstrations touched.""" - child_tables = {g.table: p for p, g in plan.children.items()} + child_tables = {g.table: g.param for g in _all_groups(plan)} list_tables = {g.table for g in plan.lists.values()} pure: dict[str, list[list[tuple[Demo, dict[str, Optional[str]]]]]] = {} counts: dict[str, list[int]] = {} @@ -638,7 +759,7 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N ) ) if linked: - g = plan.children[child_tables[ch.table]] + g = next(x for x in _all_groups(plan) if x.table == ch.table) g.changed_columns = sorted( set(g.changed_columns) | set(ch.changed_columns) ) @@ -702,7 +823,8 @@ def plan_pk(schema: Schema, table: str) -> str: def _is_param_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: - for p, g in plan.children.items(): + for g in _demo_groups(plan, d): + p = g.param if g.table != ch.table or d.values.get(p) is None or ch.after is None: continue rel = g.params[p] @@ -726,7 +848,8 @@ def _is_empty_param_row( if ch.after is None or a.after is None: return False - for p, g in plan.children.items(): + for g in _demo_groups(plan, d): + p = g.param if g.table != ch.table or d.values.get(p) is not None: continue if ch.after.get(g.link.column) != a.after.get(g.link.ref_column): @@ -748,7 +871,8 @@ def _is_ref_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: if ch.after is None: return False - for p, g in plan.children.items(): + for g in _demo_groups(plan, d): + p = g.param rel = g.params[p] if not rel.hops or rel.hops[0].table != ch.table or d.values.get(p) is None: continue @@ -782,11 +906,18 @@ def __init__(self, schema: Schema) -> None: self.schema = schema self.d = schema.dialect self.params: dict[str, dict[str, str]] = {} + #: Tables read through a view (the SQL guard refuses their name). + self.views: dict[str, str] = {} self._n = itertools.count() def q(self, name: str) -> str: return store.quote(self.d, name) + def tref(self, table: str) -> str: + """A table as a read names it: its view when it has one.""" + + return self.q(self.views.get(table, table)) + def ph(self, param: str) -> str: for k, v in self.params.items(): if v == {"param": param}: @@ -831,7 +962,7 @@ def path(self, alias: str, table: str, loc: Location, depth: int = 0) -> str: sub = f"a{depth + 1}" inner = self.path(sub, hop.table, rest, depth + 1) return ( - f"(SELECT {inner} FROM {self.q(hop.table)} {sub} WHERE " + f"(SELECT {inner} FROM {self.tref(hop.table)} {sub} WHERE " f"{self.same(f'{sub}.{self.q(hop.ref_column)}', f'{alias}.{self.q(hop.column)}', hop.declared)})" ) @@ -897,7 +1028,7 @@ def copied( self, alias: str, detail: Mapping[str, Any], table: str, column: str ) -> str: src = ( - f"(SELECT s.{self.q(detail['column'])} FROM {self.q(detail['table'])} s " + f"(SELECT s.{self.q(detail['column'])} FROM {self.tref(detail['table'])} s " f"WHERE s.{self.q(detail['ref_column'])} = {alias}.{self.q(detail['via'])})" ) ref = f"{alias}.{self.q(column)}" @@ -1003,6 +1134,7 @@ def build_contract( """One ``openadapt.verify/v1`` document for one field set.""" b = SqlBuilder(schema) + b.views = dict(settings.views) fields = set(variant.fields) anchor = plan.anchor surfaces: dict[str, Surface] = {} @@ -1066,7 +1198,8 @@ def build_contract( found = flavor == "found" groups = [ (p, g) - for p, g in plan.children.items() + for g in _all_groups(plan, variant.dispatch_value) + for p in [g.param] if p in fields or (not found and g.empty_when_null) or (not found and plan.mode == "create" and set(g.co_owners) & fields) @@ -1497,7 +1630,7 @@ def variants( for d in plan.demos: given = { k for k, v in d.values.items() if v is not None and not isinstance(v, list) - } & (located | set(plan.children)) + } & (located | {g.param for g in plan.children.values()}) base_sets.append(frozenset(given)) optional = [p for p in plan.optional if p in located and p not in lists] seen: set[frozenset[str]] = set() @@ -1845,6 +1978,9 @@ def propose( questions: list[Question] = [] _share_locations(schema, plans, kinds) + _break_ties_across(plans) + for p in plans: # child groups of values placed after analysis + _children(schema, p, [], only_missing=True) location_questions: list[Question] = [] # Q: per kind, a value the generator couldn't place. for p in plans: @@ -1855,9 +1991,7 @@ def propose( for name, tied in p.unresolved.items(): pick = chosen.get(name) if isinstance(pick, int) and 0 <= pick < len(tied): - p.locations[name] = tied[pick] - if p.mode == "update" and name not in p.identity + p.changes: - p.changes.append(name) + _resolve(p, name, tied[pick]) else: open_[name] = tied asked = dict(p.unresolved) @@ -2066,6 +2200,10 @@ def propose( "placed_from_another_kind": sorted(p.shared), "demonstrations": len(p.demos), "children": {k: g.table for k, g in p.children.items()}, + "dispatch_children": { + dv: {k: g.table for k, g in gs.items()} + for dv, gs in p.dispatch_children.items() + }, "lists": {k: g.table for k, g in p.lists.items()}, "side_rows": [ {"table": s.table, "count": s.count, "unless": s.unless} From a50021b9e6e1d680c978426574bc1b47f54bb5ac Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:44:46 -0400 Subject: [PATCH 19/94] WIP: derived-copy tie-break; logs per kind --- .../generate/verify_contract.py | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 90240c1cea..1cad59aef4 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -359,7 +359,7 @@ def analyze( if len(by_value) > 1: ok = True for dv, per in by_value.items(): - loc, tied = _pick(p, _intersect(per)) + loc, tied = _pick(p, _intersect(per), plan.demos) if loc is None: key = f"{p} (when {plan.dispatch} is {dv})" plan.unresolved[key] = tied @@ -371,7 +371,7 @@ def analyze( plan.changes.append(p) continue all_found = _intersect(found[p]) - loc, tied = _pick(p, all_found) + loc, tied = _pick(p, all_found, plan.demos) if loc is not None: plan.extra[p] = [ x @@ -480,10 +480,13 @@ def _share_locations( def _pick( - param: str, found: Sequence[Location] + param: str, found: Sequence[Location], demos: Sequence[Demo] = () ) -> tuple[Optional[Location], list[Location]]: - """The best location; among tied ones, the one whose column is named - like the parameter (``stage``, not ``last_stage``), if exactly one is.""" + """The best location. Among tied ones: the one whose column is named + like the parameter (``stage``, not ``last_stage``); else drop a column + that is only a copy of another tied one, lower-cased or digits only, in + every row of its table (EspoCRM's ``email_address.lower``, + ``phone_number.numeric``).""" loc, tied = best(found) if loc is not None or not tied: @@ -492,9 +495,38 @@ def _pick( named = [t for t in tied if t.columns and t.columns[-1] == name] if len(named) == 1: return named[0], [] + keep = [t for t in tied if not _derived_copy(t, tied, demos)] + if len(keep) == 1: + return keep[0], [] return None, tied +def _derived_copy( + loc: Location, tied: Sequence[Location], demos: Sequence[Demo] +) -> bool: + if len(loc.columns) != 1 or not loc.hops or not demos: + return False + table = loc.hops[-1].table + rows = demos[0].after.get(table) + if rows is None or rows.count_only is not None or not rows.rows: + return False + + def digits(v: str) -> str: + return "".join(ch for ch in v if ch.isdigit()) + + for other in tied: + if other is loc or other.hops != loc.hops or len(other.columns) != 1: + continue + a, b = other.columns[0], loc.columns[0] + for fn in (str.lower, digits): + if all( + r.get(b) == (fn(r[a]) if r.get(a) is not None else None) + for r in rows.rows.values() + ) and any(r.get(a) != r.get(b) for r in rows.rows.values()): + return True + return False + + def _resolve(plan: KindPlan, key: str, loc: Location) -> None: """Place an unresolved value (answered, or settled by a tie-break).""" @@ -2032,16 +2064,22 @@ def propose( for col, role in _anchor_roles(schema, p, v, actor_params).items(): if role.role == "varies": unchecked.setdefault(p.anchor, set()).add(col) + log_add: set[str] = set() + log_remove: set[str] = set() ans = _answer(answers, "bookkeeping") if isinstance(ans, dict): - logs = (logs | set(ans.get("logs_add", []))) - set(ans.get("logs_remove", [])) + log_add, log_remove = ( + set(ans.get("logs_add", [])), + set(ans.get("logs_remove", [])), + ) + logs = (logs | log_add) - log_remove for t, cols in dict(ans.get("columns_add", {})).items(): book.setdefault(t, set()).update(cols) for t, cols in dict(ans.get("columns_remove", {})).items(): book[t] = book.get(t, set()) - set(cols) sides = sorted({f"{s.table} ({s.count} per write)" for p in plans for s in p.sides}) for p in plans: # a table the person calls a log is no longer counted - p.sides = [s for s in p.sides if s.table not in logs] + p.sides = [s for s in p.sides if s.table not in log_add] if logs or book or unchecked or sides: lines = [] if sides: @@ -2239,7 +2277,8 @@ def propose( continue ok = True for flavor in flavors: - doc = build_contract(schema, p, v, flavor, settings, book, logs) + kind_logs = (p.logs | log_add) - log_remove + doc = build_contract(schema, p, v, flavor, settings, book, kind_logs) rel = Path("contracts") / p.kind / f"{v.slug}.{flavor}.yaml" path = out / rel path.parent.mkdir(parents=True, exist_ok=True) From bea29dfb4fabbd798aec03f6b7d9ac9db13a45fc Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:47:08 -0400 Subject: [PATCH 20/94] WIP: identical columns tie-break --- .../generate/verify_contract.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 1cad59aef4..ffd7e168dc 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -498,9 +498,41 @@ def _pick( keep = [t for t in tied if not _derived_copy(t, tied, demos)] if len(keep) == 1: return keep[0], [] + same = _identical_columns(keep, demos) + if same is not None: + return same, [] return None, tied +def _identical_columns( + tied: Sequence[Location], demos: Sequence[Demo] +) -> Optional[Location]: + """Tied columns of one row that hold the same value in every row of + their table can't be told apart by any data: keep the one the table + defines first (the source; a copy is added after it). Either compares + exactly, so a wrong pick stops a write, never passes one.""" + + if not demos or len(tied) < 2: + return None + first = tied[0] + if any( + t.hops != first.hops or len(t.columns) != 1 or t.rule != first.rule + for t in tied + ): + return None + table = first.hops[-1].table if first.hops else None + rows = demos[0].after.get(table) if table else None + if rows is None or rows.count_only is not None or not rows.rows: + return None + cols = [t.columns[0] for t in tied] + if not all(len({r.get(c) for c in cols}) == 1 for r in rows.rows.values()): + return None + order = list(next(iter(rows.rows.values())).keys()) + return min( + tied, key=lambda t: order.index(t.columns[0]) if t.columns[0] in order else 999 + ) + + def _derived_copy( loc: Location, tied: Sequence[Location], demos: Sequence[Demo] ) -> bool: From 3fdcb366a92751b027bc3da600d6f8b45ed3d9f9 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:50:49 -0400 Subject: [PATCH 21/94] WIP: views in their own schema --- .../generate/verify_contract.py | 79 +++++++++++++------ 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index ffd7e168dc..f9463912a7 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -980,7 +980,12 @@ def q(self, name: str) -> str: def tref(self, table: str) -> str: """A table as a read names it: its view when it has one.""" - return self.q(self.views.get(table, table)) + return self.qt(self.views.get(table, table)) + + def qt(self, name: str) -> str: + """A table name, ``schema.table`` quoted part by part.""" + + return ".".join(self.q(part) for part in name.split(".")) def ph(self, param: str) -> str: for k, v in self.params.items(): @@ -1124,7 +1129,7 @@ class Surface: def sql(self, b: SqlBuilder) -> str: cols = ", ".join(f"{expr} AS {b.q(a)}" for a, expr in self.columns) - return f"SELECT {cols} FROM {b.q(self.table)} {self.alias}" + return f"SELECT {cols} FROM {b.qt(self.table)} {self.alias}" def _base_columns(b: SqlBuilder, table: str, alias: str = "t") -> list[tuple[str, str]]: @@ -1959,29 +1964,61 @@ def _sees_triggers(connect: Any) -> bool: return False +def view_schema(schema: Schema) -> str: + """Where the generator's views live: a schema of their own, beside the + app's, so the app's own upgrades and restores never drop them and the + whole-database frame doesn't read them as app tables.""" + + return f"{schema.database}_oa_read" + + +def existing_views( + connect: Any, schema: Schema, tables: Sequence[str] +) -> dict[str, str]: + """Table -> ``schema.view`` for the views the read account can see.""" + + conn = connect() + try: + cur = conn.cursor() + cur.execute( + "SELECT table_name FROM information_schema.views WHERE table_schema = %s", + (view_schema(schema),), + ) + have = {str(r[0]) for r in cur.fetchall()} + finally: + conn.rollback() + conn.close() + return { + t: f"{view_schema(schema)}.oa_read_{t}" + for t in tables + if f"oa_read_{t}" in have + } + + def create_views( admin_read: Mapping[str, Any], schema: Schema, tables: Sequence[str], reader: str ) -> dict[str, str]: - """Create one read-only view per table the SQL guard refuses by name, - in the same database, and grant the read account SELECT on it. Only run - with ``--create-views``; the connection is the admin one given.""" + """Create one read-only view per table the SQL guard refuses by name, in + the schema ``_oa_read``, and grant the read account SELECT on + that schema. Only run with ``--create-views``; the connection is the + admin one given.""" + if schema.dialect != "mysql": + raise GenerateError("--create-views supports MySQL and MariaDB") conn = _admin_connect(admin_read) made = {} + vs = store.quote("mysql", view_schema(schema)) try: cur = conn.cursor() + cur.execute(f"CREATE DATABASE IF NOT EXISTS {vs}") for t in tables: - view = f"oa_read_{t}" - q = store.quote(schema.dialect, t) - v = store.quote(schema.dialect, view) - cur.execute(f"CREATE OR REPLACE VIEW {v} AS SELECT * FROM {q}") - if schema.dialect == "mysql": - cur.execute(f"GRANT SELECT ON {v} TO {reader}") - else: - cur.execute( - f"GRANT SELECT ON {v} TO {store.quote('postgresql', reader)}" - ) - made[t] = view + q = store.quote("mysql", schema.database) + "." + store.quote("mysql", t) + v = vs + "." + store.quote("mysql", f"oa_read_{t}") + cur.execute( + f"CREATE OR REPLACE SQL SECURITY DEFINER VIEW {v} AS SELECT * FROM {q}" + ) + made[t] = f"{view_schema(schema)}.oa_read_{t}" + cur.execute(f"GRANT SELECT ON {vs}.* TO {reader}") conn.commit() finally: conn.close() @@ -2200,18 +2237,16 @@ def propose( } ) if needed: - existing = {t: f"oa_read_{t}" for t in needed if f"oa_read_{t}" in schema.views} + existing = existing_views(connect, schema, needed) missing = [t for t in needed if t not in existing] if missing and create_views_flag: if not admin_read or not reader_account: raise GenerateError( "--create-views needs --admin-read and --reader-account" ) - existing.update(create_views(admin_read, schema, missing, reader_account)) - schema = store.read_schema(connect, dialect) - for t, v in list(existing.items()): - if v not in schema.views: - existing.pop(t) + made = create_views(admin_read, schema, missing, reader_account) + applied.extend(f"view {v} over {t}" for t, v in made.items()) + existing = existing_views(connect, schema, needed) settings.views = existing for p in plans: tables = {p.anchor} | { From 3e9c5e1eff069443bf9909a5ce16d09491d233f4 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:53:36 -0400 Subject: [PATCH 22/94] WIP: new referenced rows per dispatch value --- .../openadapt_flow/generate/verify_contract.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index f9463912a7..8b4476ba02 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -946,8 +946,16 @@ def _is_ref_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: and ch.after.get(col) is not None and _rule_ok(ch.after.get(col), d.values[p], rel.rule) ): + dv = str(d.values.get(plan.dispatch)) if plan.dispatch else None plan.added_refs.setdefault( - p, {"table": ch.table, "column": col, "rule": rel.rule} + f"{p}@{dv}" if dv else p, + { + "table": ch.table, + "column": col, + "rule": rel.rule, + "param": p, + "dv": dv, + }, ) return True return False @@ -1336,8 +1344,9 @@ def build_contract( if plan.mode == "update": target_fields.update(g.changed_columns) # A new row a link row names (a new email address). - for p, ref in plan.added_refs.items(): - if p not in fields or found: + for ref in plan.added_refs.values(): + p = ref["param"] + if p not in fields or found or ref["dv"] not in (None, variant.dispatch_value): continue effects.append( _raw_effect( From 6c41eea3b5c695942f1980f3b5f4a35ef76d0fe4 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:07:32 -0400 Subject: [PATCH 23/94] WIP: replaced linked rows declared with their count; demonstrated sets first; schema column order for ties --- .../openadapt_flow/generate/demo_analysis.py | 2 +- .../generate/verify_contract.py | 80 ++++++++++++++++--- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 7eee2fcf2f..f90ad9a67f 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -374,7 +374,7 @@ def walk( target_row = idx.row(target, v) if target_row is None or (target == anchor.table and hops): continue # back to the main record: not a location - if not declared and not hops and (target, v) in added: + if not declared and not hops and anchor.key and (target, v) in added: # The main row's key columns point at rows that were # there before; a row the write added is reached from a # row that names the main record instead. diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 8b4476ba02..914fe80bab 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -198,6 +198,10 @@ class ChildGroup: #: The parameter whose value the rows hold (``children`` may hold a #: second group for it: a call's lead is its parent and an attendee). param: str = "" + #: An update replaced earlier rows here (removed or changed rows linked + #: to the record), leaving ``after_count`` of them. + replaced: bool = False + after_count: int = 1 @dataclass @@ -359,7 +363,7 @@ def analyze( if len(by_value) > 1: ok = True for dv, per in by_value.items(): - loc, tied = _pick(p, _intersect(per), plan.demos) + loc, tied = _pick(p, _intersect(per), plan.demos, schema) if loc is None: key = f"{p} (when {plan.dispatch} is {dv})" plan.unresolved[key] = tied @@ -371,7 +375,7 @@ def analyze( plan.changes.append(p) continue all_found = _intersect(found[p]) - loc, tied = _pick(p, all_found, plan.demos) + loc, tied = _pick(p, all_found, plan.demos, schema) if loc is not None: plan.extra[p] = [ x @@ -480,7 +484,10 @@ def _share_locations( def _pick( - param: str, found: Sequence[Location], demos: Sequence[Demo] = () + param: str, + found: Sequence[Location], + demos: Sequence[Demo] = (), + schema: Optional[Schema] = None, ) -> tuple[Optional[Location], list[Location]]: """The best location. Among tied ones: the one whose column is named like the parameter (``stage``, not ``last_stage``); else drop a column @@ -498,14 +505,14 @@ def _pick( keep = [t for t in tied if not _derived_copy(t, tied, demos)] if len(keep) == 1: return keep[0], [] - same = _identical_columns(keep, demos) + same = _identical_columns(keep, demos, schema) if same is not None: return same, [] return None, tied def _identical_columns( - tied: Sequence[Location], demos: Sequence[Demo] + tied: Sequence[Location], demos: Sequence[Demo], schema: Optional[Schema] = None ) -> Optional[Location]: """Tied columns of one row that hold the same value in every row of their table can't be told apart by any data: keep the one the table @@ -527,7 +534,9 @@ def _identical_columns( cols = [t.columns[0] for t in tied] if not all(len({r.get(c) for c in cols}) == 1 for r in rows.rows.values()): return None - order = list(next(iter(rows.rows.values())).keys()) + if schema is None or table not in schema.tables: + return None + order = schema.tables[table].column_names return min( tied, key=lambda t: order.index(t.columns[0]) if t.columns[0] in order else 999 ) @@ -810,6 +819,7 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N continue if ch.kind == "removed": if ch.table in child_tables or ch.table in list_tables: + _mark_replaced(schema, plan, d, a, ch) continue plan.removed[ch.table] = plan.removed.get(ch.table, 0) + 1 continue @@ -823,6 +833,7 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N ) ) if linked: + _mark_replaced(schema, plan, d, a, ch) g = next(x for x in _all_groups(plan) if x.table == ch.table) g.changed_columns = sorted( set(g.changed_columns) | set(ch.changed_columns) @@ -881,6 +892,30 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N plan.bookkeeping.setdefault(plan.anchor, set()).update(extra) +def _mark_replaced( + schema: Schema, plan: KindPlan, d: Demo, a: RowChange, ch: RowChange +) -> None: + """An update removed or changed a row linked to the record in a table + that holds a value: count the record's rows there after the write.""" + + if plan.mode != "update" or a.after is None: + return + for g in _demo_groups(plan, d): + if g.table != ch.table: + continue + key = a.after.get(g.link.ref_column) + rows = d.after.get(g.table) + if rows is None or rows.count_only is not None: + continue + consts = [(c, r.value) for c, r in g.roles.items() if r.role == "constant"] + n = sum( + 1 + for r in rows.rows.values() + if r.get(g.link.column) == key and all(r.get(c) == v for c, v in consts) + ) + g.replaced, g.after_count = True, n + + def plan_pk(schema: Schema, table: str) -> str: pk = schema.tables[table].primary_key return pk[0] if len(pk) == 1 else "" @@ -1341,6 +1376,24 @@ def build_contract( # Found: the row is already there, and nothing changes. effect["count_new_only"] = True effects.append(effect) + if plan.mode == "update" and g.replaced and not found: + # The demonstrations replaced the record's earlier rows here (an + # old email address link removed): afterwards the record has + # exactly as many such rows as they left, and its earlier ones + # may go. + m = { + k: v + for k, v in effects[-1]["match"].items() + if k.startswith(("main__", "pin__")) or k in g.roles + } + effects.append( + { + "kind": "record_written", + "probe": f"surface={sname}|the record's rows here after the write", + "match": m, + "expected_count": g.after_count, + } + ) if plan.mode == "update": target_fields.update(g.changed_columns) # A new row a link row names (a new email address). @@ -1711,12 +1764,15 @@ def variants( } & (located | {g.param for g in plan.children.values()}) base_sets.append(frozenset(given)) optional = [p for p in plan.optional if p in located and p not in lists] - seen: set[frozenset[str]] = set() - sets: list[frozenset[str]] = [] - for base in base_sets: - drop = [p for p in optional if p in base] - for n in range(0, len(drop) + 1): - for combo in itertools.combinations(drop, n): + seen: set[frozenset[str]] = set(base_sets) + # Every demonstrated set first, then sets with fewer optional fields, + # one field left out at a time across demonstrations, so the cap keeps + # the sets closest to what was shown. + sets: list[frozenset[str]] = list(dict.fromkeys(base_sets)) + drops = {base: [p for p in optional if p in base] for base in base_sets} + for n in range(1, max((len(v) for v in drops.values()), default=0) + 1): + for base in base_sets: + for combo in itertools.combinations(drops[base], n): s = frozenset(base - set(combo)) if s not in seen: seen.add(s) From 43fbadce4059b88edb25c67abf0f96af912ceb91 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:12:19 -0400 Subject: [PATCH 24/94] WIP: empty pins only where the app left the column empty; generated tokens unchecked; co-owners not via the main record --- .../generate/verify_contract.py | 78 ++++++++++++++----- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 914fe80bab..1e1c904fbe 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -691,7 +691,11 @@ def _children( and not isinstance(d.values.get(q), (list, dict)) ] if hits and all( - h and h[0].hops and _hop_cost(h[0].hops[0]) <= 1 for h in hits + h + and h[0].hops + and _hop_cost(h[0].hops[0]) <= 1 + and h[0].hops[0].table != plan.anchor + for h in hits ): co.append(q) explained |= {h[0].hops[0].column for h in hits} @@ -1280,20 +1284,31 @@ def build_contract( roles = _anchor_roles(schema, plan, variant, settings.actor_params) for col, role in sorted(roles.items()): _pin(b, main, anchor, col, role, match, settings.actor_params) + support = _support(plan, variant) for p in plan.optional: if p in fields or p in variant.lists: continue loc = plan.locations.get(p) if loc is None: continue - if not loc.hops: - for c in loc.columns: - main.columns.append((f"empty__{c}", b.empty("t", anchor, c))) - match[f"empty__{c}"] = {"literal": "1"} - elif loc.hops[0].kind == "fk": - c = loc.hops[0].column - main.columns.append((f"empty__{c}", b.empty("t", anchor, c))) - match[f"empty__{c}"] = {"literal": "1"} + cols = ( + list(loc.columns) + if not loc.hops + else ([loc.hops[0].column] if loc.hops[0].kind == "fk" else []) + ) + for c in cols: + # A column another value of this field set fills (a call's + # parent is its account, or its lead) is not pinned empty. + if any( + (a.after or {}).get(c) not in (None, "") + for d, a in support + if d.values.get(p) is None + ): + continue + alias = _add_column( + main, f"empty__{_alias(c)}", b.empty("t", anchor, c) + ) + match[alias] = {"literal": "1"} effect: dict[str, Any] = { "kind": "record_written", "probe": "surface=record|the main record holds the task's values", @@ -1698,14 +1713,7 @@ def _anchor_roles( """The main row's columns no parameter explains, from the demonstrations that give this field set (all of them when none gives exactly it).""" - fields = set(variant.fields) | set(variant.lists) - if variant.dispatch_value is not None: - fields.add(plan.dispatch or "") - support = [ - (d, a) - for d, a in zip(plan.demos, plan.anchors) - if fields <= {k for k, v in d.values.items() if v is not None} - ] or list(zip(plan.demos, plan.anchors)) + support = _support(plan, variant) explained: set[str] = set() for loc in plan.locations.values(): if not loc.hops: @@ -1713,7 +1721,41 @@ def _anchor_roles( elif loc.hops[0].kind == "fk": explained.add(loc.hops[0].column) rows = [(d, a.after or {}) for d, a in support] - return column_roles(schema, plan.anchor, rows, explained, actor_params) + roles = column_roles(schema, plan.anchor, rows, explained, actor_params) + if len(support) < len(plan.demos): + # A column that differs between all demonstrations and holds a + # generated token (a UUID) is the app's own, whichever ones support + # this field set. + everyone = [(d, a.after or {}) for d, a in zip(plan.demos, plan.anchors)] + for col, role in column_roles( + schema, plan.anchor, everyone, explained, actor_params + ).items(): + values = [r.get(col) for _, r in everyone] + if role.role == "varies" and all( + v is not None and _TOKEN.match(v) for v in values + ): + roles[col] = role + return roles + + +_TOKEN = re.compile( + r"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + r"|[0-9a-fA-F]{16,})$" +) + + +def _support(plan: KindPlan, variant: Variant) -> list[tuple[Demo, RowChange]]: + """The demonstrations that give every field of this set (all of them + when none does).""" + + fields = set(variant.fields) | set(variant.lists) + if variant.dispatch_value is not None: + fields.add(plan.dispatch or "") + return [ + (d, a) + for d, a in zip(plan.demos, plan.anchors) + if fields <= {k for k, v in d.values.items() if v is not None} + ] or list(zip(plan.demos, plan.anchors)) # -- variants ----------------------------------------------------------------------- From c68b85b82512a235da994a8805dc468b177a1e4d Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:24:35 -0400 Subject: [PATCH 25/94] docs: generator decision rules --- packages/flow/docs/GENERATE_CONTRACTS.md | 65 ++++++++++++++++++------ 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 821f0597cb..4db99d3513 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -126,23 +126,43 @@ that as a stop: the write must not be sent unchecked. | A display column of the row a key column points to | `project_id` -> `projects.name` | | Two text columns joined by one space | `firstname` and `lastname` | | A row the write added that holds the main record's key | a journal note, a custom field value | +| A row it added that links the record to the row holding the value | an email address, a call's attendee | A key column can be a declared foreign key, or a column whose value is another table's primary key. When several places fit in every demonstration, the generator keeps the shortest path, then a declared key, then a column -named after its table, then the most exact rule. If places are still tied, -that is a question. - -An update parameter that no example gives, such as `new_zip`, is placed at the -main table's column with the same name after a leading `new_` or `set_`, when -there is one. The report lists such parameters under `placed_by_name`. +named after its table, then the most exact rule. Among places still tied, it +keeps, in this order: + +1. the column named like the parameter (`stage`, not `last_stage`); +2. the source column when another is its lower-cased or digits-only copy in + every row (`email_address.name`, not `email_address.lower`), or the column + the table defines first when they hold the same value in every row; +3. the place another kind's demonstrations used for the same table. + +If places are still tied, that is a question. A value that is also in a row +the write added beside the record (a call's lead is its parent and an +attendee row) is checked in both places. + +A parameter that no example gives is placed where another kind's +demonstrations found the parameter of the same name on the same main table +(`placed_from_another_kind`), else at the main table's column of the same +name after a leading `new_` or `set_` (`placed_by_name`). A parameter whose +value names the field to change (`field: status`, `value: In Process`) gets a +contract per field: the demonstrated ones, and every other text column of the +main table. **Field sets.** A task's field set is its non-null values. For an update, the generator writes a contract for every non-empty set of the fields to change that it found, up to 64. For a create, it writes one per demonstrated set, -plus the sets with an optional field (null in some example) left out. A -field left out of a create must be empty in the new record. A list parameter -gets a contract per length, from 1 to 10 items. +then the sets with optional fields left out, up to 64 in all. A field is +optional when some example leaves it null or its place can be empty (a +nullable column, a row beside the record). A field left out of a create must +be empty in the new record, unless the demonstrations that left it out +filled that column anyway (a call's parent, which is its account when there +is no lead). A row the app adds with an empty value when a field is left out +(Redmine's custom field values, a journal with no note) is expected then +too. A list parameter gets a contract per length, from 1 to 10 items. **Create, found, update.** @@ -154,15 +174,27 @@ gets a contract per length, from 1 to 10 items. record with the task's values is already there: a "make sure it exists" task whose record exists, or an update that is already in place. - `update`: exactly one record matches the task's identity values and holds - the new values. Only the fields the task changes may change. + the new values. Only the fields the task changes may change. When the + demonstrations replaced the record's rows beside it (an old email address + link removed), the earlier rows may go, and afterwards the record has as + many such rows as the demonstrations left. + +**Columns of a new row no value explains.** A key, a time stamp from the +write, the acting user, a link to a row the write added, the year, month, +week or day of a date the task gives, a generated token (a UUID), or a copy +of a column of the row a key points to (an invoice line's price from its +product) is recognized as such. Free text the demonstrations left empty is +not checked. Any other column must hold the value the demonstrations +showed; one that differed between them is not checked and is listed in the +bookkeeping question. **Rows the app adds on its own.** A row type the app added the same number of times in every demonstration (an agenda event, a stream note, a price-history row) is declared with that count and its constant columns. A table whose added rows vary between demonstrations, or whose name says log, history, -audit or similar, is proposed as a log. Columns that changed with no task -value (a modified time, a version counter) are proposed as bookkeeping. -Both go in one question. +audit or similar, is proposed as a log of that kind. Columns that changed +with no task value (a modified time, a version counter) are proposed as +bookkeeping. All of it goes in one question. **The frame.** On MySQL and MariaDB every contract uses the whole-database frame (`frame.database`): a row added, removed or changed in any table that @@ -189,8 +221,11 @@ and an admin connection you give, or not written, with the reason. needs it only to see them. - `--create-views --admin-read admin.yaml --reader-account ...`: the verifier's SQL guard refuses some table names in a read (EspoCRM's `call`). - This creates a view `oa_read_
` and grants the read account `SELECT` - on it. Without the flag, the kind is refused with the reason. + This creates a view `oa_read_
` in a schema of its own, + `_oa_read` (so the app's own restores and upgrades don't drop it), + and grants the read account `SELECT` on that schema. Without the flag, the + kind is refused with the reason. `report.json` lists what was applied under + `applied`. Every contract is loaded and read once against the live store before it is written out. A contract whose read fails is not written; `report.json` says From 86d6f9d03b48d4fc35cb5950c5a2acc4140f1b88 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:34:01 -0400 Subject: [PATCH 26/94] WIP: created rows' tables append-only in the whole-database frame, so the recheck sees them --- packages/flow/openadapt_flow/generate/verify_contract.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 1e1c904fbe..8992e467ba 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1483,6 +1483,15 @@ def build_contract( continue allow.append({"table": t}) for t in sorted(named_tables): + if plan.mode == "create" or flavor == "found": + # The contract's own read judges the rows the write adds; the + # whole-database frame still sees any existing row change, + # now and at the recheck (a late edit to a look-alike). + allow.append({"table": t}) + continue + # An update changes a row here, which an append-only table + # can't allow: its own read judges this table, and the recheck + # (database only) doesn't see it. allow.append( { "table": t, From 0a2abe07fbc6cb23b68910400404608b3f9c24e6 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:53:44 -0400 Subject: [PATCH 27/94] WIP: email and phone values always compared in canonical form; docs link --- packages/flow/docs/VERIFY_WRITES.md | 5 +++++ .../openadapt_flow/generate/demo_analysis.py | 20 ++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/flow/docs/VERIFY_WRITES.md b/packages/flow/docs/VERIFY_WRITES.md index 5bcc461af6..f71443376a 100644 --- a/packages/flow/docs/VERIFY_WRITES.md +++ b/packages/flow/docs/VERIFY_WRITES.md @@ -26,6 +26,11 @@ effects: The contract belongs to whoever runs the agent. The agent can't change what it is checked against. +When the app keeps its data in MySQL, MariaDB or PostgreSQL, the generator +writes these contracts from the schema and one demonstration write per +example, and asks a person only what it can't decide: +[GENERATE_CONTRACTS.md](GENERATE_CONTRACTS.md). + ## 2. Wrap the write In Python, wrap your agent's own write function. The same code works as a tool in the Claude Agent SDK, OpenAI Agents, LangGraph, or a plain script: diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index f90ad9a67f..eb576f8158 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -47,6 +47,8 @@ _IDLIKE = re.compile(r"(^id$|_id$|^id_|^fk_|_fk$|^parent$|^parent_id$)", re.I) _IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +_EMAIL = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") +_PHONE = re.compile(r"^\+?[0-9][0-9 ().\-/]*$") #: Table names an app's own logs usually carry. Only a proposal: a person #: confirms which tables are logs (the bookkeeping question). _LOG_NAME = re.compile( @@ -99,19 +101,23 @@ def rule_for(stored: Optional[str], column_kind: str, value: Any) -> Optional[st text = value_text(value) if text == "": return None + if column_kind == "text": + # An email address or a phone number is compared in its declared + # canonical form even when this demonstration's text matches + # exactly: another write may store ``Ana@Example.org`` or + # ``+16175550144`` for the same value. + if _EMAIL.match(text) and stored.casefold() == text.casefold(): + return "email" + if _PHONE.match(text) and sum(ch.isdigit() for ch in text) >= 6: + a, b = kind_form("phone", stored), kind_form("phone", text) + if a is not None and a == b: + return "phone" if stored == text and column_kind not in ("number", "bool", "date", "datetime"): # A number or date column is compared by value even when this # demonstration's text happens to match: another row of the same # column may print as ``5.5000``. return "exact" if column_kind == "text": - if "@" in text and stored.casefold() == text.casefold(): - return "email" - digits = sum(ch.isdigit() for ch in text) - if digits >= 6: - a, b = kind_form("phone", stored), kind_form("phone", text) - if a is not None and a == b: - return "phone" return None if column_kind in ("number", "bool"): a, b = _decimal(stored), _decimal(text) From 34aba77d286ec6a15c5b4a94bca04a32ebc826c4 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:57:02 -0400 Subject: [PATCH 28/94] WIP: answered location questions still counted --- packages/flow/docs/GENERATE_CONTRACTS.md | 4 ++-- packages/flow/openadapt_flow/generate/verify_contract.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 4db99d3513..0b6d28a41c 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -119,8 +119,8 @@ that as a stop: the write must not be sent unchecked. | Rule | Example | | --- | --- | | The stored text equals the value | `subject` | -| Equal after case folding (an email address) | `Ana@Example.org` | -| Equal digits (a phone number) | `02 38 53 21 09` stored as `0238532109` | +| Equal after case folding, for any value shaped like an email address | `Ana@Example.org` | +| Equal digits, for any value shaped like a phone number | `02 38 53 21 09` stored as `0238532109` | | Equal as a number, a date or a date-time, compared in SQL | `9.20` stored as `9.2`; `2026-10-27 14:30` stored as `2026-10-27 14:30:00` | | Minutes between two date-time columns of the row | a call's duration | | A display column of the row a key column points to | `project_id` -> `projects.name` | diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 8992e467ba..612ac09b51 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -2196,22 +2196,20 @@ def propose( _share_locations(schema, plans, kinds) _break_ties_across(plans) - for p in plans: # child groups of values placed after analysis - _children(schema, p, [], only_missing=True) location_questions: list[Question] = [] # Q: per kind, a value the generator couldn't place. for p in plans: if not p.unresolved: continue chosen = dict(_answer(answers, f"locations.{p.kind}") or {}) + asked = dict(p.unresolved) open_ = {} - for name, tied in p.unresolved.items(): + for name, tied in list(p.unresolved.items()): pick = chosen.get(name) if isinstance(pick, int) and 0 <= pick < len(tied): _resolve(p, name, tied[pick]) else: open_[name] = tied - asked = dict(p.unresolved) p.unresolved = open_ if asked: parts = [] @@ -2328,6 +2326,8 @@ def propose( DEFAULT_RECHECK_S, ) ) + for p in plans: # child groups of values placed after analysis + _children(schema, p, [], only_missing=True) questions.extend(location_questions) questions_open = [q for q in questions if _answer(answers, q.key) is None] settings = Settings( From ba048d05f1e9461b5765cb81fd1644b351fd6d8c Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 17:16:05 -0400 Subject: [PATCH 29/94] WIP: Dolibarr results --- .../results/dolibarr/QUESTIONS.md | 6 +- ...l+name+phone+town+zip.8386ba67.create.yaml | 6 +- ...il+name+phone+town+zip.8386ba67.found.yaml | 3 +- ...ice_date+town+linesx2.17de9df9.create.yaml | 44 +- ...oice_date+town+linesx2.17de9df9.found.yaml | 81 ++- ...customer+town+linesx2.c91f9518.create.yaml | 484 -------------- .../customer+town+linesx2.c91f9518.found.yaml | 88 --- ...excl_vat+ref+vat_rate.bcd72fd0.create.yaml | 234 +++++-- ..._excl_vat+ref+vat_rate.bcd72fd0.found.yaml | 69 +- ...w_email+new_phone+town.be743cce.found.yaml | 101 --- ..._email+new_phone+town.be743cce.update.yaml | 218 ------ ...w_email+new_phone+town.110f7554.found.yaml | 3 +- ..._email+new_phone+town.110f7554.update.yaml | 4 +- .../results/dolibarr/manifest.yaml | 625 ++++++++++++++++++ .../results/dolibarr/report.json | 28 +- 15 files changed, 969 insertions(+), 1025 deletions(-) delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md index 92a0749f46..336f371750 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md @@ -1,6 +1,6 @@ # Questions for a person -1. Is this the app's own bookkeeping, which a write may change without a task asking? Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. +1. Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): llx_actioncomm (1 per write), llx_actioncomm_resources (1 per write). Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. - Answer key: `bookkeeping`. Proposed: `accept`. - Answered: `"accept"` @@ -14,7 +14,7 @@ ## Kinds -- `create_customer` (create, main table `llx_societe`): 1 contract(s). -- `create_product` (create, main table `llx_product`): 1 contract(s). +- `create_customer` (create, main table `llx_societe`): 64 contract(s). +- `create_product` (create, main table `llx_product`): 4 contract(s). - `create_draft_invoice` (create, main table `llx_facture`): 10 contract(s). - `update_customer` (update, main table `llx_societe`): 15 contract(s). diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml index 095d9bcd5b..bf2b55e636 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml @@ -151,6 +151,7 @@ effects: param: country email: param: email + kind: email nom: param: name phone: @@ -255,8 +256,6 @@ effects: - kind: record_written probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm match: - code: - literal: AC_COMPANY_CREATE entity: literal: '1' event_paid: @@ -308,8 +307,6 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_societe - mode: skip - reason: read in full, every column, by this contract's own surface views: llx_actioncomm: raw_llx_actioncomm llx_actioncomm_resources: raw_llx_actioncomm_resources @@ -317,6 +314,7 @@ frame: settle_attempts: 5 new_row_fields: llx_actioncomm: + - code - datec - datep - datep2 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml index c54cbd55ef..c00bc365fc 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml @@ -74,6 +74,7 @@ effects: param: country email: param: email + kind: email nom: param: name phone: @@ -98,8 +99,6 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_societe - mode: skip - reason: read in full, every column, by this contract's own surface views: {} settle_s: 1 settle_attempts: 5 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml index 46aa29ec26..f2931d37f3 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml @@ -169,44 +169,8 @@ verifier: `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, CASE WHEN CAST(%(v2)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_1_qty`, CASE WHEN - t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE - WHEN t.`date_end` IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS - NULL THEN '1' ELSE '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_multicurrency` = (SELECT - s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' - END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE '0' END AS - `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, - CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, - CASE WHEN t.`fk_remise_except` IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE - WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` - = (SELECT s.`fk_user_author` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE - '0' END AS `pin__fk_user_author`, CASE WHEN t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__info_bits`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, - CASE WHEN t.`multicurrency_subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` - = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE - WHEN t.`price` IS NULL THEN '1' ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT - s.`type` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, - CASE WHEN t.`rang` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN - t.`remise` = (SELECT s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' - ELSE '0' END AS `pin__remise`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__situation_percent`, CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__special_code`, CASE WHEN t.`subprice` = (SELECT s.`price` FROM - `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__subprice`, CASE - WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__subprice_ttc`, - CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) - THEN '1' ELSE '0' END AS `pin__total_localtax1`, CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax2`, - CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) - THEN '1' ELSE '0' END AS `pin__tva_tx` FROM `llx_facturedet` t + AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_1_qty` FROM `llx_facturedet` + t sql_query_params: v0: *id002 v1: @@ -480,11 +444,7 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_facture - mode: skip - reason: read in full, every column, by this contract's own surface - table: llx_facturedet - mode: skip - reason: read in full, every column, by this contract's own surface views: {} settle_s: 1 settle_attempts: 5 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml index e9e6d0f41e..7db480bfb8 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml @@ -54,8 +54,56 @@ verifier: = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, (SELECT CAST(a1.`town` AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town` FROM `llx_facture` t sql_query_params: - v0: + v0: &id002 param: invoice_date + rows_llx_facturedet: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_facture` + AS CHAR) AS `fk_facture`, CAST(t.`fk_parent_line` AS CHAR) AS `fk_parent_line`, CAST(t.`fk_product` + AS CHAR) AS `fk_product`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS + `description`, CAST(t.`vat_src_code` AS CHAR) AS `vat_src_code`, CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, + CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, + CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, + CAST(t.`qty` AS CHAR) AS `qty`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise` + AS CHAR) AS `remise`, CAST(t.`fk_remise_except` AS CHAR) AS `fk_remise_except`, CAST(t.`subprice` + AS CHAR) AS `subprice`, CAST(t.`subprice_ttc` AS CHAR) AS `subprice_ttc`, CAST(t.`price` AS CHAR) + AS `price`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, + CAST(t.`total_localtax1` AS CHAR) AS `total_localtax1`, CAST(t.`total_localtax2` AS CHAR) AS `total_localtax2`, + CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, CAST(t.`product_type` AS CHAR) AS `product_type`, + CAST(t.`date_start` AS CHAR) AS `date_start`, CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`info_bits` + AS CHAR) AS `info_bits`, CAST(t.`buy_price_ht` AS CHAR) AS `buy_price_ht`, CAST(t.`fk_product_fournisseur_price` + AS CHAR) AS `fk_product_fournisseur_price`, CAST(t.`batch` AS CHAR) AS `batch`, CAST(t.`fk_warehouse` + AS CHAR) AS `fk_warehouse`, CAST(t.`special_code` AS CHAR) AS `special_code`, CAST(t.`rang` AS + CHAR) AS `rang`, CAST(t.`fk_contract_line` AS CHAR) AS `fk_contract_line`, CAST(t.`fk_unit` AS + CHAR) AS `fk_unit`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_code_ventilation` + AS CHAR) AS `fk_code_ventilation`, CAST(t.`situation_percent` AS CHAR) AS `situation_percent`, + CAST(t.`fk_prev_id` AS CHAR) AS `fk_prev_id`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, + CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_subprice` + AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, + CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` + AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CAST(a2.`nom` AS CHAR) FROM `llx_societe` + a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS + `main__customer`, CASE WHEN (SELECT a1.`datef` FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) + = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `main__invoice_date`, (SELECT (SELECT CAST(a2.`town` + AS CHAR) FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` + = t.`fk_facture`) AS `main__town`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE + CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_0_product_ref`, CASE + WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v1)s AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `p__lines_0_qty`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 + WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, + CASE WHEN CAST(%(v2)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `p__lines_1_qty` FROM `llx_facturedet` t + sql_query_params: + v0: *id002 + v1: + param: lines.0.qty + v2: + param: lines.1.qty effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -67,6 +115,34 @@ effects: p__town: param: town expected_count: 1 +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__invoice_date: + literal: '1' + main__town: + param: town + p__lines_0_product_ref: + param: lines.0.product_ref + p__lines_0_qty: + literal: '1' + expected_count: 1 +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__invoice_date: + literal: '1' + main__town: + param: town + p__lines_1_product_ref: + param: lines.1.product_ref + p__lines_1_qty: + literal: '1' + expected_count: 1 frame: ignore_fields: - fk_user_modif @@ -80,8 +156,7 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_facture - mode: skip - reason: read in full, every column, by this contract's own surface + - table: llx_facturedet views: {} settle_s: 1 settle_attempts: 5 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml deleted file mode 100644 index 18d772fe81..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml +++ /dev/null @@ -1,484 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, create, fields customer, town, linesx2. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` - AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, - CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` - AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, - CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` - AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, - CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` - AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) - AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` - AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) - AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS - `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, - CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, - CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, - CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, - CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, - CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, - CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` - AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` - AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, - CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS - `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS - `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) - AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` - AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` - AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, - CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS - `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, - CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, - CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, - CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) - AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, - CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` - AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, (SELECT CAST(a1.`town` - AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town`, CASE WHEN t.`close_missing_amount` - IS NULL THEN '1' ELSE '0' END AS `pin__close_missing_amount`, CASE WHEN t.`date_closing` IS NULL - THEN '1' ELSE '0' END AS `pin__date_closing`, CASE WHEN t.`date_pointoftax` IS NULL THEN '1' ELSE - '0' END AS `pin__date_pointoftax`, CASE WHEN t.`date_valid` IS NULL THEN '1' ELSE '0' END AS `pin__date_valid`, - CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE - WHEN t.`fk_account` IS NULL THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_cond_reglement` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_cond_reglement`, CASE WHEN t.`fk_fac_rec_source` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_fac_rec_source`, CASE WHEN t.`fk_facture_source` IS - NULL THEN '1' ELSE '0' END AS `pin__fk_facture_source`, CASE WHEN t.`fk_incoterms` = (SELECT s.`fk_incoterms` - FROM `llx_societe` s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_incoterms`, - CASE WHEN t.`fk_mode_reglement` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_mode_reglement`, - CASE WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_societe` s WHERE s.`rowid` - = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_projet` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_projet`, CASE WHEN t.`fk_statut` = (SELECT s.`statut` FROM `llx_societe` - s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_statut`, CASE WHEN t.`fk_transport_mode` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_transport_mode`, CASE WHEN t.`fk_user_closing` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_user_closing`, CASE WHEN t.`fk_user_modif` IS NULL THEN '1' - ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_user_valid` IS NULL THEN '1' ELSE '0' END - AS `pin__fk_user_valid`, CASE WHEN t.`is_also_delivery_note` = CAST('0' AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `pin__is_also_delivery_note`, CASE WHEN t.`localtax1` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax1`, CASE WHEN t.`localtax2` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax2`, CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, CASE WHEN t.`paye` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__paye`, CASE WHEN t.`prorata_discount` IS NULL THEN '1' ELSE '0' - END AS `pin__prorata_discount`, CASE WHEN t.`remise` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE - '0' END AS `pin__remise`, CASE WHEN t.`remise_absolue` = CAST('0' AS DECIMAL(65,30)) THEN '1' - ELSE '0' END AS `pin__remise_absolue`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`retained_warranty` = CAST('0' AS - DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty`, CASE WHEN t.`retained_warranty_date_limit` - IS NULL THEN '1' ELSE '0' END AS `pin__retained_warranty_date_limit`, CASE WHEN t.`retained_warranty_fk_cond_reglement` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty_fk_cond_reglement`, - CASE WHEN t.`revenuestamp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__revenuestamp`, - CASE WHEN t.`situation_counter` IS NULL THEN '1' ELSE '0' END AS `pin__situation_counter`, CASE - WHEN t.`situation_cycle_ref` IS NULL THEN '1' ELSE '0' END AS `pin__situation_cycle_ref`, CASE - WHEN t.`situation_final` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_final`, - CASE WHEN t.`subtype` IS NULL THEN '1' ELSE '0' END AS `pin__subtype`, CASE WHEN t.`type` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__type` FROM `llx_facture` t - rows_llx_facturedet: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_facture` - AS CHAR) AS `fk_facture`, CAST(t.`fk_parent_line` AS CHAR) AS `fk_parent_line`, CAST(t.`fk_product` - AS CHAR) AS `fk_product`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS - `description`, CAST(t.`vat_src_code` AS CHAR) AS `vat_src_code`, CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, - CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, - CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, - CAST(t.`qty` AS CHAR) AS `qty`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise` - AS CHAR) AS `remise`, CAST(t.`fk_remise_except` AS CHAR) AS `fk_remise_except`, CAST(t.`subprice` - AS CHAR) AS `subprice`, CAST(t.`subprice_ttc` AS CHAR) AS `subprice_ttc`, CAST(t.`price` AS CHAR) - AS `price`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, - CAST(t.`total_localtax1` AS CHAR) AS `total_localtax1`, CAST(t.`total_localtax2` AS CHAR) AS `total_localtax2`, - CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, CAST(t.`product_type` AS CHAR) AS `product_type`, - CAST(t.`date_start` AS CHAR) AS `date_start`, CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`info_bits` - AS CHAR) AS `info_bits`, CAST(t.`buy_price_ht` AS CHAR) AS `buy_price_ht`, CAST(t.`fk_product_fournisseur_price` - AS CHAR) AS `fk_product_fournisseur_price`, CAST(t.`batch` AS CHAR) AS `batch`, CAST(t.`fk_warehouse` - AS CHAR) AS `fk_warehouse`, CAST(t.`special_code` AS CHAR) AS `special_code`, CAST(t.`rang` AS - CHAR) AS `rang`, CAST(t.`fk_contract_line` AS CHAR) AS `fk_contract_line`, CAST(t.`fk_unit` AS - CHAR) AS `fk_unit`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_code_ventilation` - AS CHAR) AS `fk_code_ventilation`, CAST(t.`situation_percent` AS CHAR) AS `situation_percent`, - CAST(t.`fk_prev_id` AS CHAR) AS `fk_prev_id`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, - CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, - CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_subprice` - AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, - CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` - AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CAST(a2.`nom` AS CHAR) FROM `llx_societe` - a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS - `main__customer`, (SELECT (SELECT CAST(a2.`town` AS CHAR) FROM `llx_societe` a2 WHERE a2.`rowid` - = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS `main__town`, (SELECT - CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` - AS BINARY)) AS `p__lines_0_product_ref`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND t.`qty` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_0_qty`, CASE WHEN - t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE - WHEN t.`date_end` IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS - NULL THEN '1' ELSE '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_multicurrency` = (SELECT - s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' - END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE '0' END AS - `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, - CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, - CASE WHEN t.`fk_remise_except` IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE - WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` - = (SELECT s.`fk_user_author` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE - '0' END AS `pin__fk_user_author`, CASE WHEN t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__info_bits`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, - CASE WHEN t.`multicurrency_subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` - = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE - WHEN t.`price` IS NULL THEN '1' ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT - s.`type` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, - CASE WHEN t.`rang` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN - t.`remise` = (SELECT s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' - ELSE '0' END AS `pin__remise`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__situation_percent`, CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__special_code`, CASE WHEN t.`subprice` = (SELECT s.`price` FROM - `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__subprice`, CASE - WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__subprice_ttc`, - CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) - THEN '1' ELSE '0' END AS `pin__total_localtax1`, CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax2`, - CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) - THEN '1' ELSE '0' END AS `pin__tva_tx`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE - CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, CASE - WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v1)s AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `p__lines_1_qty`, CASE WHEN t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE WHEN t.`date_end` IS NULL THEN '1' ELSE '0' - END AS `pin__date_end`, CASE WHEN t.`date_start` IS NULL THEN '1' ELSE '0' END AS `pin__date_start`, - CASE WHEN t.`fk_code_ventilation` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, - CASE WHEN t.`fk_contract_line` IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE - WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` - = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_prev_id`, CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, CASE WHEN t.`fk_remise_except` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE - '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` FROM `llx_facture` - s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, CASE WHEN - t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_user_modif`, - CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_warehouse`, - CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__info_bits`, - CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_tx`, - CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_tx`, - CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` FROM `llx_facture` s WHERE s.`rowid` - = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, CASE WHEN t.`multicurrency_subprice` - = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END - AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE WHEN t.`price` IS NULL THEN '1' - ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT s.`type` FROM `llx_facture` - s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, CASE WHEN t.`rang` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN t.`remise` = (SELECT - s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__remise`, - CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_percent`, - CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_percent`, - CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__special_code`, - CASE WHEN t.`subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) - THEN '1' ELSE '0' END AS `pin__subprice`, CASE WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__subprice_ttc`, CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax1`, - CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) - THEN '1' ELSE '0' END AS `pin__total_localtax2`, CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM - `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx` FROM - `llx_facturedet` t - sql_query_params: - v0: - param: lines.0.qty - v1: - param: lines.1.qty -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__customer: - param: customer - p__town: - param: town - pin__close_missing_amount: - literal: '1' - pin__date_closing: - literal: '1' - pin__date_pointoftax: - literal: '1' - pin__date_valid: - literal: '1' - pin__entity: - literal: '1' - pin__fk_account: - literal: '1' - pin__fk_cond_reglement: - literal: '1' - pin__fk_fac_rec_source: - literal: '1' - pin__fk_facture_source: - literal: '1' - pin__fk_incoterms: - literal: '1' - pin__fk_mode_reglement: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_projet: - literal: '1' - pin__fk_statut: - literal: '1' - pin__fk_transport_mode: - literal: '1' - fk_user_author: - param: bot_user_id - pin__fk_user_closing: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_user_valid: - literal: '1' - pin__is_also_delivery_note: - literal: '1' - pin__localtax1: - literal: '1' - pin__localtax2: - literal: '1' - multicurrency_code: - literal: EUR - pin__multicurrency_tx: - literal: '1' - pin__paye: - literal: '1' - pin__prorata_discount: - literal: '1' - pin__remise: - literal: '1' - pin__remise_absolue: - literal: '1' - pin__remise_percent: - literal: '1' - pin__retained_warranty: - literal: '1' - pin__retained_warranty_date_limit: - literal: '1' - pin__retained_warranty_fk_cond_reglement: - literal: '1' - pin__revenuestamp: - literal: '1' - pin__situation_counter: - literal: '1' - pin__situation_cycle_ref: - literal: '1' - pin__situation_final: - literal: '1' - pin__subtype: - literal: '1' - pin__type: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_llx_facturedet|a row beside the main record holds lines - match: - main__customer: - param: customer - main__town: - param: town - p__lines_0_product_ref: - param: lines.0.product_ref - p__lines_0_qty: - literal: '1' - pin__buy_price_ht: - literal: '1' - pin__date_end: - literal: '1' - pin__date_start: - literal: '1' - pin__fk_code_ventilation: - literal: '1' - pin__fk_contract_line: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_parent_line: - literal: '1' - pin__fk_prev_id: - literal: '1' - pin__fk_product_fournisseur_price: - literal: '1' - pin__fk_remise_except: - literal: '1' - pin__fk_unit: - literal: '1' - pin__fk_user_author: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_warehouse: - literal: '1' - pin__info_bits: - literal: '1' - pin__localtax1_tx: - literal: '1' - localtax1_type: - literal: '0' - pin__localtax2_tx: - literal: '1' - localtax2_type: - literal: '0' - pin__multicurrency_code: - literal: '1' - pin__multicurrency_subprice: - literal: '1' - pin__multicurrency_subprice_ttc: - literal: '1' - pin__price: - literal: '1' - pin__product_type: - literal: '1' - pin__rang: - literal: '1' - pin__remise: - literal: '1' - pin__remise_percent: - literal: '1' - pin__situation_percent: - literal: '1' - pin__special_code: - literal: '1' - pin__subprice: - literal: '1' - pin__subprice_ttc: - literal: '1' - pin__total_localtax1: - literal: '1' - pin__total_localtax2: - literal: '1' - pin__tva_tx: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_llx_facturedet|a row beside the main record holds lines - match: - main__customer: - param: customer - main__town: - param: town - p__lines_1_product_ref: - param: lines.1.product_ref - p__lines_1_qty: - literal: '1' - pin__buy_price_ht: - literal: '1' - pin__date_end: - literal: '1' - pin__date_start: - literal: '1' - pin__fk_code_ventilation: - literal: '1' - pin__fk_contract_line: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_parent_line: - literal: '1' - pin__fk_prev_id: - literal: '1' - pin__fk_product_fournisseur_price: - literal: '1' - pin__fk_remise_except: - literal: '1' - pin__fk_unit: - literal: '1' - pin__fk_user_author: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_warehouse: - literal: '1' - pin__info_bits: - literal: '1' - pin__localtax1_tx: - literal: '1' - localtax1_type: - literal: '0' - pin__localtax2_tx: - literal: '1' - localtax2_type: - literal: '0' - pin__multicurrency_code: - literal: '1' - pin__multicurrency_subprice: - literal: '1' - pin__multicurrency_subprice_ttc: - literal: '1' - pin__price: - literal: '1' - pin__product_type: - literal: '1' - pin__rang: - literal: '1' - pin__remise: - literal: '1' - pin__remise_percent: - literal: '1' - pin__situation_percent: - literal: '1' - pin__special_code: - literal: '1' - pin__subprice: - literal: '1' - pin__subprice_ttc: - literal: '1' - pin__total_localtax1: - literal: '1' - pin__total_localtax2: - literal: '1' - pin__tva_tx: - literal: '1' - expected_count: 1 - count_new_only: true -frame: - ignore_fields: - - fk_user_modif - - tms - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_facture - mode: skip - reason: read in full, every column, by this contract's own surface - - table: llx_facturedet - mode: skip - reason: read in full, every column, by this contract's own surface - views: {} - settle_s: 1 - settle_attempts: 5 - ignore_columns: - llx_societe: - - fk_user_modif - - socialnetworks - - tms - recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml deleted file mode 100644 index 108cdf3e93..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml +++ /dev/null @@ -1,88 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, found, fields customer, town, linesx2. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` - AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, - CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` - AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, - CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` - AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, - CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` - AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) - AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` - AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) - AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS - `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, - CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, - CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, - CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, - CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, - CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, - CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` - AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` - AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, - CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS - `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS - `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) - AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` - AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` - AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, - CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS - `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, - CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, - CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, - CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) - AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, - CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` - AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, (SELECT CAST(a1.`town` - AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town` FROM `llx_facture` - t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__customer: - param: customer - p__town: - param: town - expected_count: 1 -frame: - ignore_fields: - - fk_user_modif - - tms - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_facture - mode: skip - reason: read in full, every column, by this contract's own surface - views: {} - settle_s: 1 - settle_attempts: 5 - ignore_columns: - llx_societe: - - fk_user_modif - - socialnetworks - - tms - recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml index 74239e91f1..32e4406879 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml @@ -104,10 +104,71 @@ verifier: IS NULL THEN '1' ELSE '0' END AS `pin__width`, CASE WHEN t.`width_units` IS NULL THEN '1' ELSE '0' END AS `pin__width_units` FROM `llx_product` t sql_query_params: - v0: + v0: &id002 param: price_excl_vat - v1: + v1: &id003 param: vat_rate + rows_llx_product_price: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`entity` + AS CHAR) AS `entity`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_product` AS CHAR) AS `fk_product`, + CAST(t.`date_price` AS CHAR) AS `date_price`, CAST(t.`price_level` AS CHAR) AS `price_level`, + CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, CAST(t.`price_min` + AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, CAST(t.`price_base_type` + AS CHAR) AS `price_base_type`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, CAST(t.`tva_tx` + AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, CAST(t.`localtax1_tx` + AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, CAST(t.`localtax2_tx` + AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`price_label` AS CHAR) AS `price_label`, CAST(t.`tosell` + AS CHAR) AS `tosell`, CAST(t.`price_by_qty` AS CHAR) AS `price_by_qty`, CAST(t.`fk_price_expression` + AS CHAR) AS `fk_price_expression`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_price` AS CHAR) + AS `multicurrency_price`, CAST(t.`multicurrency_price_ttc` AS CHAR) AS `multicurrency_price_ttc`, + (SELECT CAST(a1.`label` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) AS `main__label`, + CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT a1.`price` FROM `llx_product` + a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `main__price_excl_vat`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` + = t.`fk_product`) AS `main__ref`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND (SELECT a1.`tva_tx` FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v1)s + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__vat_rate`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP + '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `p__price_excl_vat`, CASE WHEN t.`entity` = (SELECT s.`entity` FROM `llx_product` s WHERE s.`rowid` + = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fk_multicurrency` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_price_expression` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_price_expression`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` + FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, + CASE WHEN t.`localtax1_tx` = (SELECT s.`localtax1_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax1_type` = (SELECT s.`localtax1_type` + FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__localtax1_type`, + CASE WHEN t.`localtax2_tx` = (SELECT s.`localtax2_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`localtax2_type` = (SELECT s.`localtax2_type` + FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__localtax2_type`, + CASE WHEN t.`multicurrency_price` IS NULL THEN '1' ELSE '0' END AS `pin__multicurrency_price`, + CASE WHEN t.`multicurrency_price_ttc` IS NULL THEN '1' ELSE '0' END AS `pin__multicurrency_price_ttc`, + CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, + CASE WHEN t.`price_base_type` = (SELECT s.`price_base_type` FROM `llx_product` s WHERE s.`rowid` + = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__price_base_type`, CASE WHEN t.`price_by_qty` + = (SELECT s.`price_by_qty` FROM `llx_product_price` s WHERE s.`rowid` = t.`fk_user_author`) THEN + '1' ELSE '0' END AS `pin__price_by_qty`, CASE WHEN t.`price_level` = (SELECT s.`price_level` FROM + `llx_product_price` s WHERE s.`rowid` = t.`fk_user_author`) THEN '1' ELSE '0' END AS `pin__price_level`, + CASE WHEN t.`price_min` = (SELECT s.`price_min` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__price_min`, CASE WHEN t.`price_min_ttc` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__price_min_ttc`, CASE WHEN t.`recuperableonly` = (SELECT s.`recuperableonly` + FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__recuperableonly`, + CASE WHEN t.`tosell` = (SELECT s.`tosell` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__tosell`, CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` + s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx`, CASE WHEN CAST(%(v1)s + AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `p__vat_rate`, CASE WHEN t.`price` = (SELECT s.`price` FROM `llx_product` + s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__price` FROM `llx_product_price` + t + sql_query_params: + v0: *id002 + v1: *id003 raw_llx_actioncomm: kind: sql sql_driver: pymysql @@ -145,27 +206,6 @@ verifier: AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` t - raw_llx_product_price: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`entity` - AS CHAR) AS `entity`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_product` AS CHAR) AS `fk_product`, - CAST(t.`date_price` AS CHAR) AS `date_price`, CAST(t.`price_level` AS CHAR) AS `price_level`, - CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, CAST(t.`price_min` - AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, CAST(t.`price_base_type` - AS CHAR) AS `price_base_type`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, CAST(t.`tva_tx` - AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, CAST(t.`localtax1_tx` - AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, CAST(t.`localtax2_tx` - AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, CAST(t.`fk_user_author` - AS CHAR) AS `fk_user_author`, CAST(t.`price_label` AS CHAR) AS `price_label`, CAST(t.`tosell` - AS CHAR) AS `tosell`, CAST(t.`price_by_qty` AS CHAR) AS `price_by_qty`, CAST(t.`fk_price_expression` - AS CHAR) AS `fk_price_expression`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_price` AS CHAR) - AS `multicurrency_price`, CAST(t.`multicurrency_price_ttc` AS CHAR) AS `multicurrency_price_ttc` - FROM `llx_product_price` t effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -288,13 +328,115 @@ effects: literal: '1' expected_count: 1 count_new_only: true +- kind: record_written + probe: surface=rows_llx_product_price|a row beside the main record holds price_excl_vat + match: + main__label: + param: label + main__price_excl_vat: + literal: '1' + main__ref: + param: ref + main__vat_rate: + literal: '1' + p__price_excl_vat: + literal: '1' + pin__entity: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_price_expression: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__localtax1_tx: + literal: '1' + pin__localtax1_type: + literal: '1' + pin__localtax2_tx: + literal: '1' + pin__localtax2_type: + literal: '1' + pin__multicurrency_price: + literal: '1' + pin__multicurrency_price_ttc: + literal: '1' + pin__multicurrency_tx: + literal: '1' + pin__price_base_type: + literal: '1' + pin__price_by_qty: + literal: '1' + pin__price_level: + literal: '1' + pin__price_min: + literal: '1' + pin__price_min_ttc: + literal: '1' + pin__recuperableonly: + literal: '1' + pin__tosell: + literal: '1' + pin__tva_tx: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_llx_product_price|a row beside the main record holds vat_rate + match: + main__label: + param: label + main__price_excl_vat: + literal: '1' + main__ref: + param: ref + main__vat_rate: + literal: '1' + p__vat_rate: + literal: '1' + pin__entity: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_price_expression: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__localtax1_tx: + literal: '1' + pin__localtax1_type: + literal: '1' + pin__localtax2_tx: + literal: '1' + pin__localtax2_type: + literal: '1' + pin__multicurrency_price: + literal: '1' + pin__multicurrency_price_ttc: + literal: '1' + pin__multicurrency_tx: + literal: '1' + pin__price: + literal: '1' + pin__price_base_type: + literal: '1' + pin__price_by_qty: + literal: '1' + pin__price_level: + literal: '1' + pin__price_min: + literal: '1' + pin__price_min_ttc: + literal: '1' + pin__recuperableonly: + literal: '1' + pin__tosell: + literal: '1' + expected_count: 1 + count_new_only: true - kind: record_written probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm match: - code: - literal: AC_PRODUCT_CREATE - elementtype: - literal: product entity: literal: '1' event_paid: @@ -332,13 +474,6 @@ effects: literal: '0' expected_count: 1 count_new_only: true -- kind: record_written - probe: surface=raw_llx_product_price|1 row(s) the app adds to llx_product_price - match: - fk_user_author: - param: bot_user_id - expected_count: 1 - count_new_only: true frame: ignore_fields: - fk_user_modif @@ -352,20 +487,20 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_product - mode: skip - reason: read in full, every column, by this contract's own surface + - table: llx_product_price views: llx_actioncomm: raw_llx_actioncomm llx_actioncomm_resources: raw_llx_actioncomm_resources - llx_product_price: raw_llx_product_price settle_s: 1 settle_attempts: 5 new_row_fields: llx_actioncomm: + - code - datec - datep - datep2 - durationp + - elementtype - email_from - email_msgid - email_sender @@ -393,31 +528,6 @@ frame: - fk_actioncomm - rowid - transparency - llx_product_price: - - date_price - - default_vat_code - - entity - - fk_product - - import_key - - localtax1_tx - - localtax1_type - - localtax2_tx - - localtax2_type - - multicurrency_code - - multicurrency_tx - - price - - price_base_type - - price_by_qty - - price_label - - price_level - - price_min - - price_min_ttc - - price_ttc - - recuperableonly - - rowid - - tms - - tosell - - tva_tx ignore_columns: llx_societe: - fk_user_modif diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml index 9a555fc8e7..3be453e4a0 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml @@ -61,10 +61,44 @@ verifier: AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate` FROM `llx_product` t sql_query_params: - v0: + v0: &id002 param: price_excl_vat - v1: + v1: &id003 param: vat_rate + rows_llx_product_price: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`entity` + AS CHAR) AS `entity`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_product` AS CHAR) AS `fk_product`, + CAST(t.`date_price` AS CHAR) AS `date_price`, CAST(t.`price_level` AS CHAR) AS `price_level`, + CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, CAST(t.`price_min` + AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, CAST(t.`price_base_type` + AS CHAR) AS `price_base_type`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, CAST(t.`tva_tx` + AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, CAST(t.`localtax1_tx` + AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, CAST(t.`localtax2_tx` + AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`price_label` AS CHAR) AS `price_label`, CAST(t.`tosell` + AS CHAR) AS `tosell`, CAST(t.`price_by_qty` AS CHAR) AS `price_by_qty`, CAST(t.`fk_price_expression` + AS CHAR) AS `fk_price_expression`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_price` AS CHAR) + AS `multicurrency_price`, CAST(t.`multicurrency_price_ttc` AS CHAR) AS `multicurrency_price_ttc`, + (SELECT CAST(a1.`label` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) AS `main__label`, + CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT a1.`price` FROM `llx_product` + a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `main__price_excl_vat`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` + = t.`fk_product`) AS `main__ref`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND (SELECT a1.`tva_tx` FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v1)s + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__vat_rate`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP + '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `p__price_excl_vat`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`tva_tx` + = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate` FROM `llx_product_price` + t + sql_query_params: + v0: *id002 + v1: *id003 effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -78,6 +112,34 @@ effects: p__vat_rate: literal: '1' expected_count: 1 +- kind: record_written + probe: surface=rows_llx_product_price|a row beside the main record holds price_excl_vat + match: + main__label: + param: label + main__price_excl_vat: + literal: '1' + main__ref: + param: ref + main__vat_rate: + literal: '1' + p__price_excl_vat: + literal: '1' + expected_count: 1 +- kind: record_written + probe: surface=rows_llx_product_price|a row beside the main record holds vat_rate + match: + main__label: + param: label + main__price_excl_vat: + literal: '1' + main__ref: + param: ref + main__vat_rate: + literal: '1' + p__vat_rate: + literal: '1' + expected_count: 1 frame: ignore_fields: - fk_user_modif @@ -91,8 +153,7 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_product - mode: skip - reason: read in full, every column, by this contract's own surface + - table: llx_product_price views: {} settle_s: 1 settle_attempts: 5 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml deleted file mode 100644 index 0536988a03..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml +++ /dev/null @@ -1,101 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind update_customer, found, fields customer, new_address, new_email, new_phone, town. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` - AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` - AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, - CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) - AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, - CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS - CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, - CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) - AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS - `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) - AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS - `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` - AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, - CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, - CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, - CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` - AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` - AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS - `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, - CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, - CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, - CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` - AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` - AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` - AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` - AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` - AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` - AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` - AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` - AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, - CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` - AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` - AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` - AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` - AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` - AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` - AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` - AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` - AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) - AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) - AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` - AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, - CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS - CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` - t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - nom: - param: customer - address: - param: new_address - email: - param: new_email - phone: - param: new_phone - kind: phone - town: - param: town - expected_count: 1 -frame: - ignore_fields: - - fk_user_modif - - socialnetworks - - tms - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_societe - mode: skip - reason: read in full, every column, by this contract's own surface - views: {} - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml deleted file mode 100644 index 788c0c1431..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml +++ /dev/null @@ -1,218 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind update_customer, update, fields customer, new_address, new_email, new_phone, town. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` - AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` - AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, - CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) - AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, - CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS - CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, - CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) - AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS - `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) - AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS - `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` - AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, - CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, - CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, - CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` - AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` - AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS - `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, - CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, - CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, - CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` - AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` - AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` - AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` - AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` - AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` - AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` - AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` - AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, - CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` - AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` - AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` - AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` - AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` - AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` - AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` - AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` - AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) - AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) - AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` - AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, - CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS - CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` - t - raw_llx_actioncomm: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` - AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, - CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` - AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` - AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` - AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS - `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, - CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, - CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` - AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) - AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, - CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` - AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` - AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` - AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS - CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) - AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, - CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` - AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS - `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` - AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` - AS CHAR) AS `extraparams` FROM `llx_actioncomm` t - raw_llx_actioncomm_resources: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` - AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` - AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` - AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` - t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - nom: - param: customer - address: - param: new_address - email: - param: new_email - phone: - param: new_phone - kind: phone - town: - param: town - expected_count: 1 -- kind: record_written - probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm - match: - entity: - literal: '1' - event_paid: - literal: '0' - fk_action: - literal: '40' - fk_parent: - literal: '0' - fk_user_action: - param: bot_user_id - fk_user_author: - param: bot_user_id - fulldayevent: - literal: '0' - percent: - literal: '-1' - priority: - literal: '0' - status: - literal: '0' - transparency: - literal: '0' - visibility: - literal: default - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources - match: - element_type: - literal: user - fk_element: - param: bot_user_id - mandatory: - literal: '0' - expected_count: 1 - count_new_only: true -frame: - ignore_fields: - - fk_user_modif - - socialnetworks - - tms - target_fields: - - address - - email - - phone - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_societe - mode: skip - reason: read in full, every column, by this contract's own surface - views: - llx_actioncomm: raw_llx_actioncomm - llx_actioncomm_resources: raw_llx_actioncomm_resources - settle_s: 1 - settle_attempts: 5 - new_row_fields: - llx_actioncomm: - - code - - datec - - datep - - datep2 - - durationp - - elementtype - - email_from - - email_msgid - - email_sender - - email_subject - - email_to - - email_tobcc - - email_tocc - - errors_to - - extraparams - - fk_soc - - id - - import_key - - ip - - label - - location - - note - - recurid - - recurrule - - ref - - ref_ext - - reply_to - - tms - llx_actioncomm_resources: - - answer_status - - fk_actioncomm - - rowid - - transparency - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml index 2d06c7591d..6b5b5cbc46 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml @@ -71,6 +71,7 @@ effects: param: customer email: param: new_email + kind: email phone: param: new_phone kind: phone @@ -91,8 +92,6 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_societe - mode: skip - reason: read in full, every column, by this contract's own surface views: {} settle_s: 1 settle_attempts: 5 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml index 6d51c78306..ac485d5716 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml @@ -108,6 +108,7 @@ effects: param: customer email: param: new_email + kind: email phone: param: new_phone kind: phone @@ -117,6 +118,8 @@ effects: - kind: record_written probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm match: + code: + literal: AC_COMPANY_MODIFY entity: literal: '1' event_paid: @@ -179,7 +182,6 @@ frame: settle_attempts: 5 new_row_fields: llx_actioncomm: - - code - datec - datep - datep2 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml index be7c828bd3..13fda668fb 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml @@ -32,6 +32,608 @@ kinds: dispatch_value: null create: contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml found: contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml + - fields: + - country + - email + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+phone+town+zip.53caf579.create.yaml + found: contracts/create_customer/country+email+name+phone+town+zip.53caf579.found.yaml + - fields: + - address + - email + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+phone+town+zip.e1f4b193.create.yaml + found: contracts/create_customer/address+email+name+phone+town+zip.e1f4b193.found.yaml + - fields: + - address + - country + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+phone+town+zip.f5e33735.create.yaml + found: contracts/create_customer/address+country+name+phone+town+zip.f5e33735.found.yaml + - fields: + - address + - country + - email + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+phone+town+zip.fbf8862c.create.yaml + found: contracts/create_customer/address+country+email+phone+town+zip.fbf8862c.found.yaml + - fields: + - address + - country + - email + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+town+zip.d1643b80.create.yaml + found: contracts/create_customer/address+country+email+name+town+zip.d1643b80.found.yaml + - fields: + - address + - country + - email + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+phone+zip.ccf08c47.create.yaml + found: contracts/create_customer/address+country+email+name+phone+zip.ccf08c47.found.yaml + - fields: + - address + - country + - email + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+phone+town.789a6442.create.yaml + found: contracts/create_customer/address+country+email+name+phone+town.789a6442.found.yaml + - fields: + - email + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/email+name+phone+town+zip.f154bfb3.create.yaml + found: contracts/create_customer/email+name+phone+town+zip.f154bfb3.found.yaml + - fields: + - country + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+name+phone+town+zip.0d88eb41.create.yaml + found: contracts/create_customer/country+name+phone+town+zip.0d88eb41.found.yaml + - fields: + - country + - email + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+phone+town+zip.4b6883d6.create.yaml + found: contracts/create_customer/country+email+phone+town+zip.4b6883d6.found.yaml + - fields: + - country + - email + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+town+zip.885cb135.create.yaml + found: contracts/create_customer/country+email+name+town+zip.885cb135.found.yaml + - fields: + - country + - email + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+phone+zip.fc167af2.create.yaml + found: contracts/create_customer/country+email+name+phone+zip.fc167af2.found.yaml + - fields: + - country + - email + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+phone+town.44f5a950.create.yaml + found: contracts/create_customer/country+email+name+phone+town.44f5a950.found.yaml + - fields: + - address + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+name+phone+town+zip.d9f5e809.create.yaml + found: contracts/create_customer/address+name+phone+town+zip.d9f5e809.found.yaml + - fields: + - address + - email + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+phone+town+zip.9a3bf199.create.yaml + found: contracts/create_customer/address+email+phone+town+zip.9a3bf199.found.yaml + - fields: + - address + - email + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+town+zip.2c8b3188.create.yaml + found: contracts/create_customer/address+email+name+town+zip.2c8b3188.found.yaml + - fields: + - address + - email + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+phone+zip.73ba341b.create.yaml + found: contracts/create_customer/address+email+name+phone+zip.73ba341b.found.yaml + - fields: + - address + - email + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+phone+town.fb28b6ed.create.yaml + found: contracts/create_customer/address+email+name+phone+town.fb28b6ed.found.yaml + - fields: + - address + - country + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+phone+town+zip.9e804046.create.yaml + found: contracts/create_customer/address+country+phone+town+zip.9e804046.found.yaml + - fields: + - address + - country + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+town+zip.a3c17aca.create.yaml + found: contracts/create_customer/address+country+name+town+zip.a3c17aca.found.yaml + - fields: + - address + - country + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+phone+zip.706c04dd.create.yaml + found: contracts/create_customer/address+country+name+phone+zip.706c04dd.found.yaml + - fields: + - address + - country + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+phone+town.5082b1a5.create.yaml + found: contracts/create_customer/address+country+name+phone+town.5082b1a5.found.yaml + - fields: + - address + - country + - email + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+town+zip.e2b6efe5.create.yaml + found: contracts/create_customer/address+country+email+town+zip.e2b6efe5.found.yaml + - fields: + - address + - country + - email + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+phone+zip.32467ede.create.yaml + found: contracts/create_customer/address+country+email+phone+zip.32467ede.found.yaml + - fields: + - address + - country + - email + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+phone+town.d258e0cb.create.yaml + found: contracts/create_customer/address+country+email+phone+town.d258e0cb.found.yaml + - fields: + - address + - country + - email + - name + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+zip.9c85e2ad.create.yaml + found: contracts/create_customer/address+country+email+name+zip.9c85e2ad.found.yaml + - fields: + - address + - country + - email + - name + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+town.7d754069.create.yaml + found: contracts/create_customer/address+country+email+name+town.7d754069.found.yaml + - fields: + - address + - country + - email + - name + - phone + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+phone.a2b4ef09.create.yaml + found: contracts/create_customer/address+country+email+name+phone.a2b4ef09.found.yaml + - fields: + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/name+phone+town+zip.fbceaff4.create.yaml + found: contracts/create_customer/name+phone+town+zip.fbceaff4.found.yaml + - fields: + - email + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/email+phone+town+zip.3535f506.create.yaml + found: contracts/create_customer/email+phone+town+zip.3535f506.found.yaml + - fields: + - email + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/email+name+town+zip.475b04b2.create.yaml + found: contracts/create_customer/email+name+town+zip.475b04b2.found.yaml + - fields: + - email + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/email+name+phone+zip.d936d504.create.yaml + found: contracts/create_customer/email+name+phone+zip.d936d504.found.yaml + - fields: + - email + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/email+name+phone+town.5328a595.create.yaml + found: contracts/create_customer/email+name+phone+town.5328a595.found.yaml + - fields: + - country + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+phone+town+zip.ac1af0a2.create.yaml + found: contracts/create_customer/country+phone+town+zip.ac1af0a2.found.yaml + - fields: + - country + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+name+town+zip.4a8420e2.create.yaml + found: contracts/create_customer/country+name+town+zip.4a8420e2.found.yaml + - fields: + - country + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+name+phone+zip.a7ae9bc0.create.yaml + found: contracts/create_customer/country+name+phone+zip.a7ae9bc0.found.yaml + - fields: + - country + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/country+name+phone+town.9c66266e.create.yaml + found: contracts/create_customer/country+name+phone+town.9c66266e.found.yaml + - fields: + - country + - email + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+town+zip.202dbae5.create.yaml + found: contracts/create_customer/country+email+town+zip.202dbae5.found.yaml + - fields: + - country + - email + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+phone+zip.23211a11.create.yaml + found: contracts/create_customer/country+email+phone+zip.23211a11.found.yaml + - fields: + - country + - email + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+phone+town.160469ad.create.yaml + found: contracts/create_customer/country+email+phone+town.160469ad.found.yaml + - fields: + - country + - email + - name + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+zip.db502169.create.yaml + found: contracts/create_customer/country+email+name+zip.db502169.found.yaml + - fields: + - country + - email + - name + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+town.bc62857c.create.yaml + found: contracts/create_customer/country+email+name+town.bc62857c.found.yaml + - fields: + - country + - email + - name + - phone + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+phone.92f6dc3e.create.yaml + found: contracts/create_customer/country+email+name+phone.92f6dc3e.found.yaml + - fields: + - address + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+phone+town+zip.476dcfe8.create.yaml + found: contracts/create_customer/address+phone+town+zip.476dcfe8.found.yaml + - fields: + - address + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+name+town+zip.3c79c185.create.yaml + found: contracts/create_customer/address+name+town+zip.3c79c185.found.yaml + - fields: + - address + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+name+phone+zip.ee0d62a0.create.yaml + found: contracts/create_customer/address+name+phone+zip.ee0d62a0.found.yaml + - fields: + - address + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+name+phone+town.c22c2f3c.create.yaml + found: contracts/create_customer/address+name+phone+town.c22c2f3c.found.yaml + - fields: + - address + - email + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+town+zip.73b7baf0.create.yaml + found: contracts/create_customer/address+email+town+zip.73b7baf0.found.yaml + - fields: + - address + - email + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+phone+zip.ea4966f9.create.yaml + found: contracts/create_customer/address+email+phone+zip.ea4966f9.found.yaml + - fields: + - address + - email + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+phone+town.0dfd0d0a.create.yaml + found: contracts/create_customer/address+email+phone+town.0dfd0d0a.found.yaml + - fields: + - address + - email + - name + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+zip.6b73f20c.create.yaml + found: contracts/create_customer/address+email+name+zip.6b73f20c.found.yaml + - fields: + - address + - email + - name + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+town.59ece868.create.yaml + found: contracts/create_customer/address+email+name+town.59ece868.found.yaml + - fields: + - address + - email + - name + - phone + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+phone.c37efe1f.create.yaml + found: contracts/create_customer/address+email+name+phone.c37efe1f.found.yaml + - fields: + - address + - country + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+town+zip.13199686.create.yaml + found: contracts/create_customer/address+country+town+zip.13199686.found.yaml + - fields: + - address + - country + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+phone+zip.5e48d952.create.yaml + found: contracts/create_customer/address+country+phone+zip.5e48d952.found.yaml + - fields: + - address + - country + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+phone+town.5d9fa0bc.create.yaml + found: contracts/create_customer/address+country+phone+town.5d9fa0bc.found.yaml + - fields: + - address + - country + - name + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+zip.0c72fe0a.create.yaml + found: contracts/create_customer/address+country+name+zip.0c72fe0a.found.yaml + - fields: + - address + - country + - name + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+town.0d9093f0.create.yaml + found: contracts/create_customer/address+country+name+town.0d9093f0.found.yaml + - fields: + - address + - country + - name + - phone + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+phone.9cb52e23.create.yaml + found: contracts/create_customer/address+country+name+phone.9cb52e23.found.yaml + - fields: + - address + - country + - email + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+zip.df797a7c.create.yaml + found: contracts/create_customer/address+country+email+zip.df797a7c.found.yaml + - fields: + - address + - country + - email + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+town.dd75ab46.create.yaml + found: contracts/create_customer/address+country+email+town.dd75ab46.found.yaml + - fields: + - address + - country + - email + - phone + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+phone.fa31e92e.create.yaml + found: contracts/create_customer/address+country+email+phone.fa31e92e.found.yaml + - fields: + - address + - country + - email + - name + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name.736afe34.create.yaml + found: contracts/create_customer/address+country+email+name.736afe34.found.yaml create_product: mode: create main_table: llx_product @@ -53,6 +655,29 @@ kinds: dispatch_value: null create: contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml found: contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml + - fields: + - label + - ref + - vat_rate + lists: {} + dispatch_value: null + create: contracts/create_product/label+ref+vat_rate.c78fc3bd.create.yaml + found: contracts/create_product/label+ref+vat_rate.c78fc3bd.found.yaml + - fields: + - label + - price_excl_vat + - ref + lists: {} + dispatch_value: null + create: contracts/create_product/label+price_excl_vat+ref.0f575d09.create.yaml + found: contracts/create_product/label+price_excl_vat+ref.0f575d09.found.yaml + - fields: + - label + - ref + lists: {} + dispatch_value: null + create: contracts/create_product/label+ref.153a9f2e.create.yaml + found: contracts/create_product/label+ref.153a9f2e.found.yaml create_draft_invoice: mode: create main_table: llx_facture diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json index be5ce10a31..916e1bc81e 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json @@ -6,7 +6,7 @@ { "number": 1, "key": "bookkeeping", - "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", + "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): llx_actioncomm (1 per write), llx_actioncomm_resources (1 per write). Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", "proposal": "accept", "answered": true }, @@ -32,15 +32,17 @@ "locations": { "address": "llx_societe.address (exact)", "country": "llx_societe.fk_pays -> llx_c_country.label (exact)", - "email": "llx_societe.email (exact)", + "email": "llx_societe.email (email)", "name": "llx_societe.nom (exact)", "phone": "llx_societe.phone (phone)", "town": "llx_societe.town (exact)", "zip": "llx_societe.zip (exact)" }, "placed_by_name": [], + "placed_from_another_kind": [], "demonstrations": 2, "children": {}, + "dispatch_children": {}, "lists": {}, "side_rows": [ { @@ -57,7 +59,7 @@ "logs": [], "refused": [], "unresolved": {}, - "emitted": 1, + "emitted": 64, "rejected": [] }, "create_product": { @@ -70,8 +72,13 @@ "vat_rate": "llx_product.tva_tx (number)" }, "placed_by_name": [], + "placed_from_another_kind": [], "demonstrations": 2, - "children": {}, + "children": { + "price_excl_vat#2": "llx_product_price", + "vat_rate#2": "llx_product_price" + }, + "dispatch_children": {}, "lists": {}, "side_rows": [ { @@ -83,17 +90,12 @@ "table": "llx_actioncomm_resources", "count": 1, "unless": null - }, - { - "table": "llx_product_price", - "count": 1, - "unless": null } ], "logs": [], "refused": [], "unresolved": {}, - "emitted": 1, + "emitted": 4, "rejected": [] }, "create_draft_invoice": { @@ -105,8 +107,10 @@ "town": "llx_facture.fk_soc -> llx_societe.town (exact)" }, "placed_by_name": [], + "placed_from_another_kind": [], "demonstrations": 2, "children": {}, + "dispatch_children": {}, "lists": { "lines": "llx_facturedet" }, @@ -123,7 +127,7 @@ "locations": { "customer": "llx_societe.nom (exact)", "new_address": "llx_societe.address (exact)", - "new_email": "llx_societe.email (exact)", + "new_email": "llx_societe.email (email)", "new_phone": "llx_societe.phone (phone)", "new_zip": "llx_societe.zip (exact)", "town": "llx_societe.town (exact)" @@ -132,8 +136,10 @@ "new_address", "new_zip" ], + "placed_from_another_kind": [], "demonstrations": 2, "children": {}, + "dispatch_children": {}, "lists": {}, "side_rows": [ { From cd1bac42e194c428661dc74956831dc993f75c99 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 17:42:28 -0400 Subject: [PATCH 30/94] WIP: frame.versions from a version column or the app's audit trail --- .../benchmark/generate_contracts/RESULTS.md | 95 +++++++++++ ...ject+subject+tracker.d268453f.create.yaml} | 63 ++++---- ...oject+subject+tracker.d268453f.found.yaml} | 153 +++++++++++------- ...ject+project+spent_on.a2ffb74d.create.yaml | 8 + ...ject+project+spent_on.a2ffb74d.found.yaml} | 22 +-- ...roject+status+subject.4aa4d777.found.yaml} | 116 +++++++------ .../results/redmine/manifest.yaml | 98 +++++------ .../results/redmine/report.json | 6 +- .../generate/verify_contract.py | 146 +++++++++++++++++ .../tests/test_generate_verify_contract.py | 4 + 10 files changed, 508 insertions(+), 203 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/RESULTS.md rename packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/{assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml => assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml} (94%) rename packages/flow/benchmark/generate_contracts/results/redmine/contracts/{log_time/activity+hours+project+spent_on.e07feee9.create.yaml => create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml} (89%) rename packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/{activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml => activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml} (98%) rename packages/flow/benchmark/generate_contracts/results/redmine/contracts/{log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml => update_issue/notes+project+status+subject.4aa4d777.found.yaml} (95%) diff --git a/packages/flow/benchmark/generate_contracts/RESULTS.md b/packages/flow/benchmark/generate_contracts/RESULTS.md new file mode 100644 index 0000000000..233ce40111 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/RESULTS.md @@ -0,0 +1,95 @@ +# Generated contracts on the first study's apps + +Written 2026-10-30. The generator (`python -m openadapt_flow.generate.verify_contract`) +was run on three of the first-contact study's apps, each on its own copy: +Dolibarr instance 1, Redmine instance 1 and EspoCRM instance 2. Each run used +only what a new customer has: + +- the app's read-only database account (the fixture's `read_access()`); +- the kinds and parameter names, and the example values, from the app's + `setup_view.yaml`; +- one demonstration write per example, made here through the app's public + REST API as the bot (`demo_.py`). An example whose record already + existed was replaced by a create with values of our own. + +The study's tasks, oracle and fault scripts were used only afterwards, by +`validate.py`, to judge the contracts: each task's reference correct write +and each planted fault, through `choose`, `begin_write` and `verify_write`. +Late-write faults also ran the recheck the contract declares. + +## Results + +| | Dolibarr | Redmine | EspoCRM | +| --- | --- | --- | --- | +| Database | MariaDB 11.4 | PostgreSQL | MariaDB | +| Questions for a person | **3** | **1** | **ESPO_Q** | +| First study, person decisions | 11 | 11 | 11 | +| Contracts written | 93 | 95 | ESPO_N | +| Correct writes confirmed (tasks and controls) | 40/43 | 35/38 | ESPO_C | +| In-scope planted faults stopped | **22/22** | **19/19** | **ESPO_I** | +| First study, in-scope faults stopped | 21/21 | 26/26 | 16/19 | +| Out-of-scope late writes stopped | 2/2 (recheck) | 0/2 (no recheck on PostgreSQL) | ESPO_O | +| `propose` time | 1.5 min | 2.5 min | ESPO_T | + +The first study measured false stops on agents' writes; here the correct +writes are the pack's reference writes and the controls, so the two false +stop rates are not the same measure. + +### Questions asked + +- **Dolibarr** (3): bookkeeping (agenda events per write, `tms`, + `fk_user_modif`, the `socialnetworks` rewrite, invoice totals and draft + reference not checked); the recheck window (300 s proposed and accepted); + `invoice_date` is in `datef` and in `date_lim_reglement` (picked `datef`). +- **Redmine** (1): bookkeeping (custom field rows per write, `journal_details` + as a log, `updated_on`, `lock_version`, `user_preferences.others`, and the + time entry's `tyear`, `tmonth`, `tweek`). No recheck question: PostgreSQL + has no whole-database frame. +- **EspoCRM** (ESPO_QN) + +Every answer was the proposal, except the one choice in Dolibarr's question 3. + +### Setup steps the generator applied + +- Dolibarr and EspoCRM: `GRANT TRIGGER ON .* TO ` + (`--grant-trigger`): the whole-database frame must see triggers. +- ESPO_SETUP + +### False stops, by cause + +| App | Case | Cause | +| --- | --- | --- | +| Dolibarr | F18 (control) | Another user's edit to an unrelated customer: no audit trail is read, so it isn't attributed | +| Dolibarr | F21 (control) | Two saves add two agenda events; the contract expects one per write | +| Dolibarr | F30 (control) | A resave of a value already in place adds an agenda event; the `found` contract allows no change | +| Redmine | T19 | Closing an issue sets `closed_on`; no demonstration closed one | +| Redmine | F21 (control) | Another user's edit to an unrelated issue: not attributed | +| Redmine | F24 (control) | Two saves add two journals | +ESPO_FS + +### Misses + +Out of scope only: Redmine F19 and F20 (late writes; PostgreSQL has no +recheck). ESPO_MISS No in-scope planted fault was confirmed on any app. + +## Reproduce + +```bash +cd packages/flow/benchmark/generate_contracts +FCG_INSTANCE=1 FCG_DOLIBARR_READER_PASSWORD=... python demo_dolibarr.py WORK +python -m openadapt_flow.generate.verify_contract propose --read WORK/read.yaml \ + --kinds WORK/kinds.yaml --demos WORK/demos --out WORK/out \ + --actor-param bot_user_id=2 --answers WORK/answers.yaml \ + --grant-trigger --admin-read admin.yaml --reader-account "'fcg_reader'@'%'" +FCG_INSTANCE=1 python validate.py dolibarr WORK --actor-id 2 +python summarize.py dolibarr WORK +``` + +`results//` holds each run's `QUESTIONS.md`, `answers.yaml`, +`report.json`, `manifest.yaml` and two example contract pairs per app (the +full set is regenerated by the commands above). The fault scripts and the +oracle stay in the study's private pack. + +Code: Dolibarr and Redmine ran on this branch at `a6b5a3915` with #1661 (the +whole-database frame fix) merged in; EspoCRM ran after the rebase on `main` +with #1661, #1662, #1663, #1665, #1666, #1668 and #1669. diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml similarity index 94% rename from packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml rename to packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml index 9ffc2e8b23..8cebbfa15d 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml @@ -1,4 +1,4 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_issue, create, fields assignee, customer_ref, due_date, environment, priority, project, subject, tracker. +# Generated by openadapt_flow.generate.verify_contract: kind create_issue, create, fields assignee, customer_ref, description, environment, priority, project, subject, tracker. # Proposed, not admitted: a person answers QUESTIONS.md first. schema_version: openadapt.verify/v1 environment_id: generated-redmine @@ -27,9 +27,8 @@ verifier: TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."firstname" AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) FROM "users" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" - AS TEXT)) AS "p__assignee", CASE WHEN t."due_date" = CAST(%(v0)s AS date) THEN '1' ELSE '0' END - AS "p__due_date", (SELECT CAST(a1."name" AS TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS - TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CAST(a1."name" AS TEXT) FROM + AS TEXT)) AS "p__assignee", (SELECT CAST(a1."name" AS TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT CAST(a1."name" AS TEXT) FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" AS TEXT)) AS "p__tracker", CASE WHEN t."category_id" IS NULL THEN '1' ELSE '0' END AS "pin__category_id", @@ -43,11 +42,8 @@ verifier: IS NULL THEN '1' ELSE '0' END AS "pin__parent_id", CASE WHEN CAST(t."rgt" AS numeric) = CAST('2' AS numeric) THEN '1' ELSE '0' END AS "pin__rgt", CASE WHEN t."start_date" IS NULL THEN '1' ELSE '0' END AS "pin__start_date", CASE WHEN CAST(t."status_id" AS numeric) = CAST('1' AS numeric) - THEN '1' ELSE '0' END AS "pin__status_id", CASE WHEN t."description" IS NULL OR t."description" - = '' THEN '1' ELSE '0' END AS "empty__description" FROM "issues" t - sql_query_params: - v0: &id002 - param: due_date + THEN '1' ELSE '0' END AS "pin__status_id", CASE WHEN t."due_date" IS NULL THEN '1' ELSE '0' END + AS "empty__due_date" FROM "issues" t rows_custom_values: kind: sql sql_driver: psycopg2 @@ -57,20 +53,18 @@ verifier: CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CAST(a2."firstname" AS TEXT) || ' ' || CAST(a2."lastname" AS TEXT) FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" AS TEXT)) FROM - "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", CASE WHEN (SELECT a1."due_date" - FROM "issues" a1 WHERE a1."id" = t."customized_id") = CAST(%(v0)s AS date) THEN '1' ELSE '0' END - AS "main__due_date", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "enumerations" a2 WHERE CAST(a2."id" - AS TEXT) = CAST(a1."priority_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") - AS "main__priority", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE CAST(a2."id" - AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") - AS "main__project", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") - AS "main__subject", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "trackers" a2 WHERE CAST(a2."id" - AS TEXT) = CAST(a1."tracker_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") - AS "main__tracker", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('1' AS numeric) THEN - '1' ELSE '0' END AS "pin__custom_field_id", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('2' - AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id__47c66b" FROM "custom_values" t - sql_query_params: - v0: *id002 + "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", (SELECT CAST(a1."description" + AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__description", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "enumerations" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."priority_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__priority", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__project", (SELECT CAST(a1."subject" + AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__subject", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "trackers" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."tracker_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__tracker", CASE WHEN CAST(t."custom_field_id" + AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id", CASE WHEN + CAST(t."custom_field_id" AS numeric) = CAST('2' AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id__47c66b" + FROM "custom_values" t raw_ar_internal_metadata: kind: sql sql_driver: psycopg2 @@ -283,6 +277,14 @@ verifier: AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" t + raw_journal_details: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" + AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) + AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t raw_journals: kind: sql sql_driver: psycopg2 @@ -580,8 +582,8 @@ effects: match: p__assignee: param: assignee - p__due_date: - literal: '1' + description: + param: description p__priority: param: priority p__project: @@ -616,7 +618,7 @@ effects: literal: '1' pin__status_id: literal: '1' - empty__description: + empty__due_date: literal: '1' expected_count: 1 count_new_only: true @@ -625,8 +627,8 @@ effects: match: main__assignee: param: assignee - main__due_date: - literal: '1' + main__description: + param: description main__priority: param: priority main__project: @@ -637,6 +639,7 @@ effects: param: tracker value: param: customer_ref + kind: phone pin__custom_field_id: literal: '1' customized_type: @@ -648,8 +651,8 @@ effects: match: main__assignee: param: assignee - main__due_date: - literal: '1' + main__description: + param: description main__priority: param: priority main__project: diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml similarity index 89% rename from packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml rename to packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml index 80afe1cb97..9802b204ef 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml @@ -1,4 +1,4 @@ -# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, hours, project, spent_on. +# Generated by openadapt_flow.generate.verify_contract: kind create_issue, found, fields assignee, customer_ref, description, environment, priority, project, subject, tracker. # Proposed, not admitted: a person answers QUESTIONS.md first. schema_version: openadapt.verify/v1 environment_id: generated-redmine @@ -15,24 +15,42 @@ verifier: port: 9606 user: oa_reader sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", - CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" - AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", - CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS - TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", - CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", - (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" - AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END - AS "p__spent_on", CASE WHEN t."comments" IS NULL OR t."comments" = '' THEN '1' ELSE '0' END AS - "empty__comments", CASE WHEN t."issue_id" IS NULL THEN '1' ELSE '0' END AS "empty__issue_id" FROM - "time_entries" t - sql_query_params: - v0: - param: hours - v1: - param: spent_on + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."firstname" + AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) FROM "users" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" + AS TEXT)) AS "p__assignee", (SELECT CAST(a1."name" AS TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CAST(a1."name" AS TEXT) FROM + "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT + CAST(a1."name" AS TEXT) FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" AS + TEXT)) AS "p__tracker" FROM "issues" t + rows_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CAST(a2."firstname" AS TEXT) || ' ' || CAST(a2."lastname" + AS TEXT) FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" AS TEXT)) FROM + "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", (SELECT CAST(a1."description" + AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__description", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "enumerations" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."priority_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__priority", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__project", (SELECT CAST(a1."subject" + AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__subject", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "trackers" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."tracker_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__tracker" FROM "custom_values" + t raw_ar_internal_metadata: kind: sql sql_driver: psycopg2 @@ -161,14 +179,6 @@ verifier: sql_password_env: REDMINE_RO_DB_PASSWORD sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS TEXT) AS "tracker_id" FROM "custom_fields_trackers" t - raw_custom_values: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", - CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t raw_documents: kind: sql sql_driver: psycopg2 @@ -253,22 +263,14 @@ verifier: AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" t - raw_issues: + raw_journal_details: kind: sql sql_driver: psycopg2 sql_connect_args: *id001 sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) - AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", - CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", - CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", - CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", - CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" - AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" - AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS - TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" + AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) + AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t raw_journals: kind: sql sql_driver: psycopg2 @@ -437,6 +439,17 @@ verifier: sql_password_env: REDMINE_RO_DB_PASSWORD sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_time_entries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t raw_tokens: kind: sql sql_driver: psycopg2 @@ -553,24 +566,56 @@ effects: - kind: record_written probe: surface=record|the main record holds the task's values match: - p__activity: - param: activity - p__hours: - literal: '1' + p__assignee: + param: assignee + description: + param: description + p__priority: + param: priority p__project: param: project - p__spent_on: - literal: '1' - author_id: - param: bot_user_id - user_id: - param: bot_user_id - empty__comments: - literal: '1' - empty__issue_id: - literal: '1' + subject: + param: subject + p__tracker: + param: tracker + expected_count: 1 +- kind: record_written + probe: surface=rows_custom_values|a row beside the main record holds customer_ref + match: + main__assignee: + param: assignee + main__description: + param: description + main__priority: + param: priority + main__project: + param: project + main__subject: + param: subject + main__tracker: + param: tracker + value: + param: customer_ref + kind: phone + expected_count: 1 +- kind: record_written + probe: surface=rows_custom_values|a row beside the main record holds environment + match: + main__assignee: + param: assignee + main__description: + param: description + main__priority: + param: priority + main__project: + param: project + main__subject: + param: subject + main__tracker: + param: tracker + value: + param: environment expected_count: 1 - count_new_only: true frame: ignore_fields: - lock_version diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml index 0de24b0cd5..a8e88de096 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml @@ -268,6 +268,14 @@ verifier: AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journal_details: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" + AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) + AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t raw_journals: kind: sql sql_driver: psycopg2 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml similarity index 98% rename from packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml rename to packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml index e1d5b82da8..93e2caefcc 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml @@ -1,4 +1,4 @@ -# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, hours, issue_subject, project, spent_on. +# Generated by openadapt_flow.generate.verify_contract: kind log_time, found, fields activity, comments, hours, issue_subject, project, spent_on. # Proposed, not admitted: a person answers QUESTIONS.md first. schema_version: openadapt.verify/v1 environment_id: generated-redmine @@ -26,8 +26,7 @@ verifier: (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."issue_id" AS TEXT)) AS "p__issue_subject", (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s - AS date) THEN '1' ELSE '0' END AS "p__spent_on", CASE WHEN t."comments" IS NULL OR t."comments" - = '' THEN '1' ELSE '0' END AS "empty__comments" FROM "time_entries" t + AS date) THEN '1' ELSE '0' END AS "p__spent_on" FROM "time_entries" t sql_query_params: v0: param: hours @@ -269,6 +268,14 @@ verifier: AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journal_details: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" + AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) + AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t raw_journals: kind: sql sql_driver: psycopg2 @@ -555,6 +562,8 @@ effects: match: p__activity: param: activity + comments: + param: comments p__hours: literal: '1' p__issue_subject: @@ -563,14 +572,7 @@ effects: param: project p__spent_on: literal: '1' - author_id: - param: bot_user_id - user_id: - param: bot_user_id - empty__comments: - literal: '1' expected_count: 1 - count_new_only: true frame: ignore_fields: - lock_version diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml similarity index 95% rename from packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml rename to packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml index edbd5dd64f..fc5336b4d8 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml @@ -1,4 +1,4 @@ -# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, comments, hours, project, spent_on. +# Generated by openadapt_flow.generate.verify_contract: kind update_issue, found, fields notes, project, status, subject. # Proposed, not admitted: a person answers QUESTIONS.md first. schema_version: openadapt.verify/v1 environment_id: generated-redmine @@ -15,23 +15,35 @@ verifier: port: 9606 user: oa_reader sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", - CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" - AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", - CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS - TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", - CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", - (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" - AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END - AS "p__spent_on", CASE WHEN t."issue_id" IS NULL THEN '1' ELSE '0' END AS "empty__issue_id" FROM - "time_entries" t - sql_query_params: - v0: - param: hours - v1: - param: spent_on + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."name" AS + TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", + (SELECT CAST(a1."name" AS TEXT) FROM "issue_statuses" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."status_id" + AS TEXT)) AS "p__status" FROM "issues" t + rows_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE + CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__project", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "issue_statuses" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."status_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__status", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__subject" FROM "journals" t raw_ar_internal_metadata: kind: sql sql_driver: psycopg2 @@ -252,32 +264,6 @@ verifier: AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" t - raw_issues: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) - AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", - CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", - CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", - CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", - CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" - AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" - AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS - TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t - raw_journals: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", - CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" - AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" - AS TEXT) AS "updated_by_id" FROM "journals" t raw_member_roles: kind: sql sql_driver: psycopg2 @@ -436,6 +422,17 @@ verifier: sql_password_env: REDMINE_RO_DB_PASSWORD sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_time_entries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t raw_tokens: kind: sql sql_driver: psycopg2 @@ -552,24 +549,25 @@ effects: - kind: record_written probe: surface=record|the main record holds the task's values match: - p__activity: - param: activity - comments: - param: comments - p__hours: - literal: '1' p__project: param: project - p__spent_on: - literal: '1' - author_id: - param: bot_user_id - user_id: - param: bot_user_id - empty__issue_id: - literal: '1' + p__status: + param: status + subject: + param: subject + expected_count: 1 +- kind: record_written + probe: surface=rows_journals|a row beside the main record holds notes + match: + main__project: + param: project + main__status: + param: status + main__subject: + param: subject + notes: + param: notes expected_count: 1 - count_new_only: true frame: ignore_fields: - lock_version diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml index aca034aa1e..6249f3d711 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml @@ -33,47 +33,38 @@ kinds: dispatch_value: null create: contracts/create_issue/assignee+due_date+priority+project+subject+tracker.cb8524d8.create.yaml found: contracts/create_issue/assignee+due_date+priority+project+subject+tracker.cb8524d8.found.yaml - - fields: - - due_date - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.create.yaml - found: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.found.yaml - fields: - assignee + - customer_ref + - environment - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.create.yaml - found: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.found.yaml + create: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.create.yaml + found: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.found.yaml - fields: + - due_date - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/priority+project+subject+tracker.a0dc8790.create.yaml - found: contracts/create_issue/priority+project+subject+tracker.a0dc8790.found.yaml + create: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.create.yaml + found: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.found.yaml - fields: - assignee - - customer_ref - - environment - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.create.yaml - found: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.found.yaml + create: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.create.yaml + found: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.found.yaml - fields: - customer_ref - environment @@ -107,6 +98,15 @@ kinds: dispatch_value: null create: contracts/create_issue/assignee+customer_ref+priority+project+subject+tracker.88f7523d.create.yaml found: contracts/create_issue/assignee+customer_ref+priority+project+subject+tracker.88f7523d.found.yaml + - fields: + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/priority+project+subject+tracker.a0dc8790.create.yaml + found: contracts/create_issue/priority+project+subject+tracker.a0dc8790.found.yaml - fields: - environment - priority @@ -164,28 +164,21 @@ kinds: create: contracts/create_issue/assignee+due_date+environment+priority+project+subject+tracker.9f2faa3a.create.yaml found: contracts/create_issue/assignee+due_date+environment+priority+project+subject+tracker.9f2faa3a.found.yaml - fields: + - assignee - customer_ref - - due_date - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.create.yaml - found: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.found.yaml - - fields: - description - - due_date + - environment - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.create.yaml - found: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.found.yaml + create: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml + found: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml - fields: + - assignee + - customer_ref - due_date - environment - priority @@ -194,33 +187,32 @@ kinds: - tracker lists: {} dispatch_value: null - create: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.create.yaml - found: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.found.yaml + create: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml + found: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.found.yaml - fields: - - assignee - - description + - customer_ref + - due_date - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.create.yaml - found: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.found.yaml + create: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.create.yaml + found: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.found.yaml - fields: - description + - due_date - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.create.yaml - found: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.found.yaml + create: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.create.yaml + found: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.found.yaml - fields: - - assignee - - customer_ref - - description + - due_date - environment - priority - project @@ -228,21 +220,19 @@ kinds: - tracker lists: {} dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml - found: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml + create: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.create.yaml + found: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.found.yaml - fields: - assignee - - customer_ref - - due_date - - environment + - description - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml - found: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.found.yaml + create: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.create.yaml + found: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.found.yaml - fields: - customer_ref - description @@ -291,6 +281,16 @@ kinds: dispatch_value: null create: contracts/create_issue/assignee+customer_ref+description+priority+project+subject+tracker.4d3aeb69.create.yaml found: contracts/create_issue/assignee+customer_ref+description+priority+project+subject+tracker.4d3aeb69.found.yaml + - fields: + - description + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.create.yaml + found: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.found.yaml - fields: - description - environment diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/report.json b/packages/flow/benchmark/generate_contracts/results/redmine/report.json index 76309b9659..188f7efd34 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/report.json +++ b/packages/flow/benchmark/generate_contracts/results/redmine/report.json @@ -17,7 +17,7 @@ "main_table": "issues", "locations": { "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", - "customer_ref": "custom_values (whose customized_id is the issues key).value (exact)", + "customer_ref": "custom_values (whose customized_id is the issues key).value (phone)", "description": "issues.description (exact)", "due_date": "issues.due_date (date)", "environment": "custom_values (whose customized_id is the issues key).value (exact)", @@ -35,6 +35,7 @@ "customer_ref": "custom_values", "environment": "custom_values" }, + "dispatch_children": {}, "lists": {}, "side_rows": [], "logs": [], @@ -67,6 +68,7 @@ "children": { "notes": "journals" }, + "dispatch_children": {}, "lists": {}, "side_rows": [], "logs": [ @@ -92,6 +94,7 @@ "placed_from_another_kind": [], "demonstrations": 2, "children": {}, + "dispatch_children": {}, "lists": {}, "side_rows": [], "logs": [], @@ -114,6 +117,7 @@ "placed_from_another_kind": [], "demonstrations": 2, "children": {}, + "dispatch_children": {}, "lists": {}, "side_rows": [ { diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 612ac09b51..43bd614da2 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -249,6 +249,11 @@ class KindPlan: by_name: list[str] = field(default_factory=list) #: Parameters placed where another kind's demonstrations found them. shared: list[str] = field(default_factory=list) + #: Where the target's versions are read (``frame.versions``): a version + #: column that steps by one per save, or an audit table naming the + #: record and the user who saved it. None when the demonstrations show + #: neither. + versions: Optional[dict[str, Any]] = None #: More places a value is in: rows the write added beside the record #: (a call's lead is its parent and also an attendee row). extra: dict[str, list[Location]] = field(default_factory=dict) @@ -894,6 +899,140 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N extra = plan.anchor_changed - explained if extra: plan.bookkeeping.setdefault(plan.anchor, set()).update(extra) + plan.versions = _audit_source(schema, plan, actor_values) or _version_column( + schema, plan + ) + + +_WHOLE_NUMBER = re.compile(r"^-?[0-9]+$") + + +def _version_column(schema: Schema, plan: KindPlan) -> Optional[dict[str, Any]]: + """A whole-number column of the main record that went up by exactly one + in every update demonstration (Redmine's ``lock_version``).""" + + t = schema.tables[plan.anchor] + if len(t.primary_key) != 1: + return None + for col in t.columns: + if col.kind != "number" or col.name in t.primary_key: + continue + ok = bool(plan.anchors) + for a in plan.anchors: + b0 = (a.before or {}).get(col.name) + b1 = (a.after or {}).get(col.name) + if not ( + b0 is not None + and b1 is not None + and _WHOLE_NUMBER.match(b0) + and _WHOLE_NUMBER.match(b1) + and int(b1) == int(b0) + 1 + ): + ok = False + break + if ok: + return {"kind": "step", "table": plan.anchor, "column": col.name} + return None + + +def _audit_source( + schema: Schema, plan: KindPlan, actor_values: Mapping[str, str] +) -> Optional[dict[str, Any]]: + """A table that gained, in every update demonstration, a row naming the + main record (a column holding its key) and the acting user (a column + holding the actor's id): the app's own audit trail of saves (EspoCRM's + ``action_history_record``, Dolibarr's agenda events).""" + + t = schema.tables[plan.anchor] + if len(t.primary_key) != 1 or not actor_values: + return None + actors = set(actor_values.values()) + found: Optional[dict[str, Any]] = None + for name in sorted(schema.tables): + table = schema.tables[name] + if name == plan.anchor or len(table.primary_key) != 1: + continue + pairs: Optional[set[tuple[str, str, str]]] = None + rows: list[dict[str, Optional[str]]] = [] + for d, a in zip(plan.demos, plan.anchors): + key = (a.after or {}).get(t.primary_key[0]) + here: set[tuple[str, str, str]] = set() + for ch in d.diff.of(name, "added"): + r = ch.after or {} + for lc, lv in r.items(): + if lv != key: + continue + for ac, av in r.items(): + if ac != lc and av in actors: + p = next(k for k, v in actor_values.items() if v == av) + here.add((lc, ac, p)) + rows.append(r) + pairs = here if pairs is None else pairs & here + if not pairs or len(pairs) != 1: + continue + lc, ac, param = next(iter(pairs)) + # Rows of the same type: their constant text and whole-number columns. + where = {} + for col in table.columns: + if col.name in (lc, ac) or col.name in table.primary_key: + continue + values = {r.get(col.name) for r in rows} + if len(values) == 1: + v = next(iter(values)) + if v is not None and (col.kind == "text" or _WHOLE_NUMBER.match(v)): + if "\\" not in v and "'" not in v and "%" not in v: + where[col.name] = v + found = { + "kind": "audit", + "table": name, + "record_column": lc, + "author_column": ac, + "actor_param": param, + "where": where, + } + break + return found + + +def _versions_frame( + b: "SqlBuilder", plan: KindPlan, settings: "Settings" +) -> Optional[dict[str, Any]]: + v = plan.versions + if v is None: + return None + pk = b.schema.tables[plan.anchor].primary_key[0] + if v["kind"] == "step": + sql = ( + f"SELECT {b.text('t', plan.anchor, pk)} AS {b.q('id')}, " + f"{b.text('t', plan.anchor, v['column'])} AS {b.q('ver')} " + f"FROM {b.tref(plan.anchor)} t" + ) + return { + "read": {"kind": "sql", **settings.read, "sql_query": sql}, + "record_field": "id", + "target_field": "id", + "version_field": "ver", + } + table = v["table"] + vpk = b.schema.tables[table].primary_key[0] + conds = " AND ".join( + f"{b.text('t', table, c)} = {b.literal(val)}" + for c, val in sorted(v["where"].items()) + ) + sql = ( + f"SELECT {b.text('t', table, v['record_column'])} AS {b.q('id')}, " + f"{b.text('t', table, vpk)} AS {b.q('ver')}, " + f"{b.text('t', table, v['author_column'])} AS {b.q('author')} " + f"FROM {b.tref(table)} t" + (f" WHERE {conds}" if conds else "") + ) + return { + "read": {"kind": "sql", **settings.read, "sql_query": sql}, + "record_field": "id", + "target_field": "id", + "version_field": "ver", + "author_field": "author", + "self": [{"param": v["actor_param"]}], + } def _mark_replaced( @@ -1476,6 +1615,12 @@ def build_contract( frame["target_fields"] = [] else: frame["target_fields"] = sorted(target_fields) + if plan.mode == "update": + # Another user's save to the target inside the window (a lost + # update) shows only in the target's versions. + versions = _versions_frame(b, plan, settings) + if versions is not None: + frame["versions"] = versions if schema.dialect == "mysql": allow: list[dict[str, Any]] = [] for t in sorted(logs): @@ -2418,6 +2563,7 @@ def propose( k: v.describe(p.anchor) for k, v in sorted(p.locations.items()) }, "placed_by_name": sorted(p.by_name), + "versions": p.versions, "placed_from_another_kind": sorted(p.shared), "demonstrations": len(p.demos), "children": {k: g.table for k, g in p.children.items()}, diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index 20475c3bf8..1d5b5aec92 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -337,6 +337,10 @@ def test_questions_and_contracts(generated: dict[str, Any]) -> None: # notes), and the due date that create_issue's demonstrations placed on # the same table: every non-empty subset gets a contract. assert kinds["update_issue"]["placed_from_another_kind"] == ["due_date"] + # The journal names the issue and the user who saved it: the update's + # contracts read it as the target's versions (a lost update shows there). + assert kinds["update_issue"]["versions"]["kind"] == "audit" + assert kinds["update_issue"]["versions"]["table"] == "journals" assert kinds["update_issue"]["emitted"] == 31, json.dumps( kinds["update_issue"], indent=1 ) From 266b26650af5b6100de203b13da2589c97081cd7 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 17:44:14 -0400 Subject: [PATCH 31/94] Generator results so far; CAPABILITIES; review brief --- CAPABILITIES.md | 2 +- llms-full.txt | 1 + .../benchmark/generate_contracts/RESULTS.md | 29 +- .../results/espocrm/QUESTIONS.md | 16 + .../results/espocrm/answers.yaml | 2 + .../results/espocrm/manifest.yaml | 2457 +++++++++++++++++ .../results/espocrm/report.json | 170 ++ 7 files changed, 2663 insertions(+), 14 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/report.json diff --git a/CAPABILITIES.md b/CAPABILITIES.md index 07d3553c05..047b38de1d 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,7 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, and a recheck window the person confirms. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies: Dolibarr 3 questions; RESULTS_PLACEHOLDER) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/llms-full.txt b/llms-full.txt index e060cc35ed..b7966432e4 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -544,6 +544,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/packages/flow/benchmark/generate_contracts/RESULTS.md b/packages/flow/benchmark/generate_contracts/RESULTS.md index 233ce40111..4c0c0e5817 100644 --- a/packages/flow/benchmark/generate_contracts/RESULTS.md +++ b/packages/flow/benchmark/generate_contracts/RESULTS.md @@ -22,14 +22,14 @@ Late-write faults also ran the recheck the contract declares. | | Dolibarr | Redmine | EspoCRM | | --- | --- | --- | --- | | Database | MariaDB 11.4 | PostgreSQL | MariaDB | -| Questions for a person | **3** | **1** | **ESPO_Q** | +| Questions for a person | **3** | **1** | **2** | | First study, person decisions | 11 | 11 | 11 | -| Contracts written | 93 | 95 | ESPO_N | -| Correct writes confirmed (tasks and controls) | 40/43 | 35/38 | ESPO_C | -| In-scope planted faults stopped | **22/22** | **19/19** | **ESPO_I** | +| Contracts written | 93 | 95 | 235 | +| Correct writes confirmed (tasks and controls) | 40/43 | 35/38 | 35/40 | +| In-scope planted faults stopped | **22/22** | **19/19** | **21/21** | | First study, in-scope faults stopped | 21/21 | 26/26 | 16/19 | -| Out-of-scope late writes stopped | 2/2 (recheck) | 0/2 (no recheck on PostgreSQL) | ESPO_O | -| `propose` time | 1.5 min | 2.5 min | ESPO_T | +| Out-of-scope late writes stopped | 2/2 (recheck) | 0/2 (no recheck on PostgreSQL) | 1/2 (recheck) | +| `propose` time | 1.5 min | 2.5 min | 2 min | The first study measured false stops on agents' writes; here the correct writes are the pack's reference writes and the controls, so the two false @@ -45,7 +45,7 @@ stop rates are not the same measure. as a log, `updated_on`, `lock_version`, `user_preferences.others`, and the time entry's `tyear`, `tmonth`, `tweek`). No recheck question: PostgreSQL has no whole-database frame. -- **EspoCRM** (ESPO_QN) +- **EspoCRM** (2): bookkeeping (stream notes, `call_user` and email link rows per write; `action_history_record`, `auth_log_record` and others as logs; `stream_updated_at`, `modified_at`); the recheck window (300 s). Every answer was the proposal, except the one choice in Dolibarr's question 3. @@ -53,7 +53,7 @@ Every answer was the proposal, except the one choice in Dolibarr's question 3. - Dolibarr and EspoCRM: `GRANT TRIGGER ON .* TO ` (`--grant-trigger`): the whole-database frame must see triggers. -- ESPO_SETUP +- EspoCRM before the SQL guard fix (#1665): a view `espocrm_oa_read.oa_read_call` (`--create-views`), because the guard refused the table name `call`. After the rebase the guard reads `call` and no view is needed. ### False stops, by cause @@ -65,12 +65,15 @@ Every answer was the proposal, except the one choice in Dolibarr's question 3. | Redmine | T19 | Closing an issue sets `closed_on`; no demonstration closed one | | Redmine | F21 (control) | Another user's edit to an unrelated issue: not attributed | | Redmine | F24 (control) | Two saves add two journals | -ESPO_FS +| EspoCRM | T21 | The reference write removes the old email address row when replacing it; the demonstration didn't | +| EspoCRM | T22 | `field: phone` has no contract: no demonstration, and phones live in another table | +| EspoCRM | F22, F27 (controls) | Another user's unrelated edit or new lead: not attributed | +| EspoCRM | F24 (control) | Mixed-case email and padded name (emails now compare case-folded) | ### Misses Out of scope only: Redmine F19 and F20 (late writes; PostgreSQL has no -recheck). ESPO_MISS No in-scope planted fault was confirmed on any app. +recheck). EspoCRM F21 (a late edit to a look-alike lead: an update's main table is read by its own surface, which the recheck doesn't read). No in-scope planted fault was confirmed on any app. ## Reproduce @@ -90,6 +93,6 @@ python summarize.py dolibarr WORK full set is regenerated by the commands above). The fault scripts and the oracle stay in the study's private pack. -Code: Dolibarr and Redmine ran on this branch at `a6b5a3915` with #1661 (the -whole-database frame fix) merged in; EspoCRM ran after the rebase on `main` -with #1661, #1662, #1663, #1665, #1666, #1668 and #1669. +Code: these runs used this branch before the rebase onto `main`, with #1661 +(the whole-database frame fix) merged in, and before `frame.versions` was +emitted. Reruns on the rebased branch are in progress. diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md new file mode 100644 index 0000000000..b176960544 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md @@ -0,0 +1,16 @@ +# Questions for a person + +1. Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): call_user (1 per write), note (1 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change): action_history_record, auth_log_record, note, note_user Columns that change on their own and are not compared: account: stream_updated_at; contact: stream_updated_at; lead: modified_at, modified_by_id, stream_updated_at Columns of a new record that differ between demonstrations and are not checked: call: parent_type, uid; opportunity: last_stage, probability Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. + - Answer key: `bookkeeping`. Proposed: `accept`. + - Answered: `"accept"` + +2. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. + - Answer key: `recheck_after_s`. Proposed: `300`. + - Answered: `300` + +## Kinds + +- `create_contact` (create, main table `contact`): 64 contract(s). +- `create_opportunity` (create, main table `opportunity`): 64 contract(s). +- `update_lead` (update, main table `lead`): 43 contract(s). +- `log_call` (create, main table `call`): 64 contract(s). diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml new file mode 100644 index 0000000000..7efebeb6dc --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml @@ -0,0 +1,2 @@ +bookkeeping: accept +recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml new file mode 100644 index 0000000000..5af4af6bac --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml @@ -0,0 +1,2457 @@ +schema_version: openadapt.contract-manifest/v1 +dialect: mysql +database: espocrm +actor_params: +- bot_user_id +questions_open: [] +kinds: + create_contact: + mode: create + main_table: contact + parameters: + - account + - account_city + - first_name + - last_name + - email + - phone + - title + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - account + - account_city + - email + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml + - fields: + - account + - account_city + - email + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+last_name.cc617045.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+last_name.cc617045.found.yaml + - fields: + - account_city + - email + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+last_name+phone+title.be4b0e8f.create.yaml + found: contracts/create_contact/account_city+email+first_name+last_name+phone+title.be4b0e8f.found.yaml + - fields: + - account + - email + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name+last_name+phone+title.ce919501.create.yaml + found: contracts/create_contact/account+email+first_name+last_name+phone+title.ce919501.found.yaml + - fields: + - account + - account_city + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name+last_name+phone+title.4ff4a105.create.yaml + found: contracts/create_contact/account+account_city+first_name+last_name+phone+title.4ff4a105.found.yaml + - fields: + - account + - account_city + - email + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+last_name+phone+title.41e87667.create.yaml + found: contracts/create_contact/account+account_city+email+last_name+phone+title.41e87667.found.yaml + - fields: + - account + - account_city + - email + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+phone+title.308596e1.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+phone+title.308596e1.found.yaml + - fields: + - account + - account_city + - email + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+last_name+title.ef6b5a09.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+last_name+title.ef6b5a09.found.yaml + - fields: + - account + - account_city + - email + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+last_name+phone.66680a4a.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+last_name+phone.66680a4a.found.yaml + - fields: + - account_city + - email + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+last_name.e363a157.create.yaml + found: contracts/create_contact/account_city+email+first_name+last_name.e363a157.found.yaml + - fields: + - account + - email + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name+last_name.8a4c37a1.create.yaml + found: contracts/create_contact/account+email+first_name+last_name.8a4c37a1.found.yaml + - fields: + - account + - account_city + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name+last_name.0890bdcf.create.yaml + found: contracts/create_contact/account+account_city+first_name+last_name.0890bdcf.found.yaml + - fields: + - account + - account_city + - email + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+last_name.0663f8b5.create.yaml + found: contracts/create_contact/account+account_city+email+last_name.0663f8b5.found.yaml + - fields: + - account + - account_city + - email + - first_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name.73be94d9.create.yaml + found: contracts/create_contact/account+account_city+email+first_name.73be94d9.found.yaml + - fields: + - email + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/email+first_name+last_name+phone+title.b657aeb5.create.yaml + found: contracts/create_contact/email+first_name+last_name+phone+title.b657aeb5.found.yaml + - fields: + - account_city + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+first_name+last_name+phone+title.dc8ca344.create.yaml + found: contracts/create_contact/account_city+first_name+last_name+phone+title.dc8ca344.found.yaml + - fields: + - account_city + - email + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+last_name+phone+title.70958d35.create.yaml + found: contracts/create_contact/account_city+email+last_name+phone+title.70958d35.found.yaml + - fields: + - account_city + - email + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+phone+title.bcc47205.create.yaml + found: contracts/create_contact/account_city+email+first_name+phone+title.bcc47205.found.yaml + - fields: + - account_city + - email + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+last_name+title.4b29e4b5.create.yaml + found: contracts/create_contact/account_city+email+first_name+last_name+title.4b29e4b5.found.yaml + - fields: + - account_city + - email + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+last_name+phone.7fc1c0d6.create.yaml + found: contracts/create_contact/account_city+email+first_name+last_name+phone.7fc1c0d6.found.yaml + - fields: + - account + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+first_name+last_name+phone+title.355dafc1.create.yaml + found: contracts/create_contact/account+first_name+last_name+phone+title.355dafc1.found.yaml + - fields: + - account + - email + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+last_name+phone+title.a4c2b999.create.yaml + found: contracts/create_contact/account+email+last_name+phone+title.a4c2b999.found.yaml + - fields: + - account + - email + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name+phone+title.8cf06d5a.create.yaml + found: contracts/create_contact/account+email+first_name+phone+title.8cf06d5a.found.yaml + - fields: + - account + - email + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name+last_name+title.e9c71bcd.create.yaml + found: contracts/create_contact/account+email+first_name+last_name+title.e9c71bcd.found.yaml + - fields: + - account + - email + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name+last_name+phone.77aad286.create.yaml + found: contracts/create_contact/account+email+first_name+last_name+phone.77aad286.found.yaml + - fields: + - account + - account_city + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+last_name+phone+title.0f22d92a.create.yaml + found: contracts/create_contact/account+account_city+last_name+phone+title.0f22d92a.found.yaml + - fields: + - account + - account_city + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name+phone+title.9c11906f.create.yaml + found: contracts/create_contact/account+account_city+first_name+phone+title.9c11906f.found.yaml + - fields: + - account + - account_city + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name+last_name+title.e278e40a.create.yaml + found: contracts/create_contact/account+account_city+first_name+last_name+title.e278e40a.found.yaml + - fields: + - account + - account_city + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name+last_name+phone.29f540dc.create.yaml + found: contracts/create_contact/account+account_city+first_name+last_name+phone.29f540dc.found.yaml + - fields: + - account + - account_city + - email + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+phone+title.96539c9c.create.yaml + found: contracts/create_contact/account+account_city+email+phone+title.96539c9c.found.yaml + - fields: + - account + - account_city + - email + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+last_name+title.58914f1d.create.yaml + found: contracts/create_contact/account+account_city+email+last_name+title.58914f1d.found.yaml + - fields: + - account + - account_city + - email + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+last_name+phone.dc15be7c.create.yaml + found: contracts/create_contact/account+account_city+email+last_name+phone.dc15be7c.found.yaml + - fields: + - account + - account_city + - email + - first_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+title.bf9dcd88.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+title.bf9dcd88.found.yaml + - fields: + - account + - account_city + - email + - first_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+phone.01d2568c.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+phone.01d2568c.found.yaml + - fields: + - email + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/email+first_name+last_name.c3fc7806.create.yaml + found: contracts/create_contact/email+first_name+last_name.c3fc7806.found.yaml + - fields: + - account_city + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+first_name+last_name.b6730c1e.create.yaml + found: contracts/create_contact/account_city+first_name+last_name.b6730c1e.found.yaml + - fields: + - account_city + - email + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+last_name.116193ea.create.yaml + found: contracts/create_contact/account_city+email+last_name.116193ea.found.yaml + - fields: + - account_city + - email + - first_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name.0382cd38.create.yaml + found: contracts/create_contact/account_city+email+first_name.0382cd38.found.yaml + - fields: + - account + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+first_name+last_name.646d1dd3.create.yaml + found: contracts/create_contact/account+first_name+last_name.646d1dd3.found.yaml + - fields: + - account + - email + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+last_name.cddfc397.create.yaml + found: contracts/create_contact/account+email+last_name.cddfc397.found.yaml + - fields: + - account + - email + - first_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name.89ddee04.create.yaml + found: contracts/create_contact/account+email+first_name.89ddee04.found.yaml + - fields: + - account + - account_city + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+last_name.31fe521c.create.yaml + found: contracts/create_contact/account+account_city+last_name.31fe521c.found.yaml + - fields: + - account + - account_city + - first_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name.94c8b13b.create.yaml + found: contracts/create_contact/account+account_city+first_name.94c8b13b.found.yaml + - fields: + - account + - account_city + - email + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email.a039b55f.create.yaml + found: contracts/create_contact/account+account_city+email.a039b55f.found.yaml + - fields: + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/first_name+last_name+phone+title.759e589f.create.yaml + found: contracts/create_contact/first_name+last_name+phone+title.759e589f.found.yaml + - fields: + - email + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/email+last_name+phone+title.df1cb621.create.yaml + found: contracts/create_contact/email+last_name+phone+title.df1cb621.found.yaml + - fields: + - email + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/email+first_name+phone+title.4eb30bed.create.yaml + found: contracts/create_contact/email+first_name+phone+title.4eb30bed.found.yaml + - fields: + - email + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/email+first_name+last_name+title.44a56b67.create.yaml + found: contracts/create_contact/email+first_name+last_name+title.44a56b67.found.yaml + - fields: + - email + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/email+first_name+last_name+phone.46fdda46.create.yaml + found: contracts/create_contact/email+first_name+last_name+phone.46fdda46.found.yaml + - fields: + - account_city + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+last_name+phone+title.88c48587.create.yaml + found: contracts/create_contact/account_city+last_name+phone+title.88c48587.found.yaml + - fields: + - account_city + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+first_name+phone+title.79e3e404.create.yaml + found: contracts/create_contact/account_city+first_name+phone+title.79e3e404.found.yaml + - fields: + - account_city + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+first_name+last_name+title.869091a0.create.yaml + found: contracts/create_contact/account_city+first_name+last_name+title.869091a0.found.yaml + - fields: + - account_city + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+first_name+last_name+phone.e7cd2a47.create.yaml + found: contracts/create_contact/account_city+first_name+last_name+phone.e7cd2a47.found.yaml + - fields: + - account_city + - email + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+phone+title.667b64eb.create.yaml + found: contracts/create_contact/account_city+email+phone+title.667b64eb.found.yaml + - fields: + - account_city + - email + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+last_name+title.72180e70.create.yaml + found: contracts/create_contact/account_city+email+last_name+title.72180e70.found.yaml + - fields: + - account_city + - email + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+last_name+phone.7cf3ab30.create.yaml + found: contracts/create_contact/account_city+email+last_name+phone.7cf3ab30.found.yaml + - fields: + - account_city + - email + - first_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+title.c501b9af.create.yaml + found: contracts/create_contact/account_city+email+first_name+title.c501b9af.found.yaml + - fields: + - account_city + - email + - first_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+phone.9350202d.create.yaml + found: contracts/create_contact/account_city+email+first_name+phone.9350202d.found.yaml + - fields: + - account + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+last_name+phone+title.6c3a70e7.create.yaml + found: contracts/create_contact/account+last_name+phone+title.6c3a70e7.found.yaml + - fields: + - account + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+first_name+phone+title.441099c8.create.yaml + found: contracts/create_contact/account+first_name+phone+title.441099c8.found.yaml + - fields: + - account + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+first_name+last_name+title.d24613f1.create.yaml + found: contracts/create_contact/account+first_name+last_name+title.d24613f1.found.yaml + - fields: + - account + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+first_name+last_name+phone.83f6dc7e.create.yaml + found: contracts/create_contact/account+first_name+last_name+phone.83f6dc7e.found.yaml + - fields: + - account + - email + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+phone+title.a4ceda1e.create.yaml + found: contracts/create_contact/account+email+phone+title.a4ceda1e.found.yaml + - fields: + - account + - email + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+last_name+title.ac86c876.create.yaml + found: contracts/create_contact/account+email+last_name+title.ac86c876.found.yaml + create_opportunity: + mode: create + main_table: opportunity + parameters: + - account + - account_city + - name + - amount + - currency + - stage + - close_date + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - account + - account_city + - amount + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml + - fields: + - account_city + - amount + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+currency+name+stage.3e8d919d.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+currency+name+stage.3e8d919d.found.yaml + - fields: + - account + - amount + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+currency+name+stage.ee61cdb0.create.yaml + found: contracts/create_opportunity/account+amount+close_date+currency+name+stage.ee61cdb0.found.yaml + - fields: + - account + - account_city + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+currency+name+stage.e6646c16.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+currency+name+stage.e6646c16.found.yaml + - fields: + - account + - account_city + - amount + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+currency+name+stage.1c3e81bb.create.yaml + found: contracts/create_opportunity/account+account_city+amount+currency+name+stage.1c3e81bb.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+name+stage.840c2e09.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+name+stage.840c2e09.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+currency+stage.2110b795.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+currency+stage.2110b795.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+currency+name.3d88d046.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+currency+name.3d88d046.found.yaml + - fields: + - amount + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/amount+close_date+currency+name+stage.636367d7.create.yaml + found: contracts/create_opportunity/amount+close_date+currency+name+stage.636367d7.found.yaml + - fields: + - account_city + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+close_date+currency+name+stage.18017a0a.create.yaml + found: contracts/create_opportunity/account_city+close_date+currency+name+stage.18017a0a.found.yaml + - fields: + - account_city + - amount + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+currency+name+stage.49fd1ee5.create.yaml + found: contracts/create_opportunity/account_city+amount+currency+name+stage.49fd1ee5.found.yaml + - fields: + - account_city + - amount + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+name+stage.74c4d8cd.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+name+stage.74c4d8cd.found.yaml + - fields: + - account_city + - amount + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+currency+stage.157c0c72.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+currency+stage.157c0c72.found.yaml + - fields: + - account_city + - amount + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+currency+name.6310e515.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+currency+name.6310e515.found.yaml + - fields: + - account + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+close_date+currency+name+stage.b4e0a253.create.yaml + found: contracts/create_opportunity/account+close_date+currency+name+stage.b4e0a253.found.yaml + - fields: + - account + - amount + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+currency+name+stage.b105c423.create.yaml + found: contracts/create_opportunity/account+amount+currency+name+stage.b105c423.found.yaml + - fields: + - account + - amount + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+name+stage.a21562d9.create.yaml + found: contracts/create_opportunity/account+amount+close_date+name+stage.a21562d9.found.yaml + - fields: + - account + - amount + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+currency+stage.c8c627a9.create.yaml + found: contracts/create_opportunity/account+amount+close_date+currency+stage.c8c627a9.found.yaml + - fields: + - account + - amount + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+currency+name.a75ae1d8.create.yaml + found: contracts/create_opportunity/account+amount+close_date+currency+name.a75ae1d8.found.yaml + - fields: + - account + - account_city + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+currency+name+stage.221bd3cc.create.yaml + found: contracts/create_opportunity/account+account_city+currency+name+stage.221bd3cc.found.yaml + - fields: + - account + - account_city + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+name+stage.bce513f8.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+name+stage.bce513f8.found.yaml + - fields: + - account + - account_city + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+currency+stage.9f763456.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+currency+stage.9f763456.found.yaml + - fields: + - account + - account_city + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+currency+name.53170b23.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+currency+name.53170b23.found.yaml + - fields: + - account + - account_city + - amount + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+name+stage.940c25af.create.yaml + found: contracts/create_opportunity/account+account_city+amount+name+stage.940c25af.found.yaml + - fields: + - account + - account_city + - amount + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+currency+stage.f84f8946.create.yaml + found: contracts/create_opportunity/account+account_city+amount+currency+stage.f84f8946.found.yaml + - fields: + - account + - account_city + - amount + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+currency+name.9d5c7892.create.yaml + found: contracts/create_opportunity/account+account_city+amount+currency+name.9d5c7892.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+stage.4fdcd48d.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+stage.4fdcd48d.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+name.c265ff6a.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+name.c265ff6a.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - currency + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+currency.19ea66ae.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+currency.19ea66ae.found.yaml + - fields: + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/close_date+currency+name+stage.6196f741.create.yaml + found: contracts/create_opportunity/close_date+currency+name+stage.6196f741.found.yaml + - fields: + - amount + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/amount+currency+name+stage.bd43a82e.create.yaml + found: contracts/create_opportunity/amount+currency+name+stage.bd43a82e.found.yaml + - fields: + - amount + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/amount+close_date+name+stage.2a87af45.create.yaml + found: contracts/create_opportunity/amount+close_date+name+stage.2a87af45.found.yaml + - fields: + - amount + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/amount+close_date+currency+stage.d62f80cd.create.yaml + found: contracts/create_opportunity/amount+close_date+currency+stage.d62f80cd.found.yaml + - fields: + - amount + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/amount+close_date+currency+name.6c521040.create.yaml + found: contracts/create_opportunity/amount+close_date+currency+name.6c521040.found.yaml + - fields: + - account_city + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+currency+name+stage.b7663184.create.yaml + found: contracts/create_opportunity/account_city+currency+name+stage.b7663184.found.yaml + - fields: + - account_city + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+close_date+name+stage.d3bed3d5.create.yaml + found: contracts/create_opportunity/account_city+close_date+name+stage.d3bed3d5.found.yaml + - fields: + - account_city + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+close_date+currency+stage.cc489929.create.yaml + found: contracts/create_opportunity/account_city+close_date+currency+stage.cc489929.found.yaml + - fields: + - account_city + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+close_date+currency+name.0ec4ae28.create.yaml + found: contracts/create_opportunity/account_city+close_date+currency+name.0ec4ae28.found.yaml + - fields: + - account_city + - amount + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+name+stage.44542cc2.create.yaml + found: contracts/create_opportunity/account_city+amount+name+stage.44542cc2.found.yaml + - fields: + - account_city + - amount + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+currency+stage.01773cbe.create.yaml + found: contracts/create_opportunity/account_city+amount+currency+stage.01773cbe.found.yaml + - fields: + - account_city + - amount + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+currency+name.964d6501.create.yaml + found: contracts/create_opportunity/account_city+amount+currency+name.964d6501.found.yaml + - fields: + - account_city + - amount + - close_date + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+stage.05eb4131.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+stage.05eb4131.found.yaml + - fields: + - account_city + - amount + - close_date + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+name.691edf71.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+name.691edf71.found.yaml + - fields: + - account_city + - amount + - close_date + - currency + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+currency.5f3eb90a.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+currency.5f3eb90a.found.yaml + - fields: + - account + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+currency+name+stage.ef841eea.create.yaml + found: contracts/create_opportunity/account+currency+name+stage.ef841eea.found.yaml + - fields: + - account + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+close_date+name+stage.9dec0d83.create.yaml + found: contracts/create_opportunity/account+close_date+name+stage.9dec0d83.found.yaml + - fields: + - account + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+close_date+currency+stage.67034ba2.create.yaml + found: contracts/create_opportunity/account+close_date+currency+stage.67034ba2.found.yaml + - fields: + - account + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+close_date+currency+name.430d7e5a.create.yaml + found: contracts/create_opportunity/account+close_date+currency+name.430d7e5a.found.yaml + - fields: + - account + - amount + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+name+stage.cc3f78e3.create.yaml + found: contracts/create_opportunity/account+amount+name+stage.cc3f78e3.found.yaml + - fields: + - account + - amount + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+currency+stage.d361f29b.create.yaml + found: contracts/create_opportunity/account+amount+currency+stage.d361f29b.found.yaml + - fields: + - account + - amount + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+currency+name.9346646b.create.yaml + found: contracts/create_opportunity/account+amount+currency+name.9346646b.found.yaml + - fields: + - account + - amount + - close_date + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+stage.b840114e.create.yaml + found: contracts/create_opportunity/account+amount+close_date+stage.b840114e.found.yaml + - fields: + - account + - amount + - close_date + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+name.510970cc.create.yaml + found: contracts/create_opportunity/account+amount+close_date+name.510970cc.found.yaml + - fields: + - account + - amount + - close_date + - currency + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+currency.add6b978.create.yaml + found: contracts/create_opportunity/account+amount+close_date+currency.add6b978.found.yaml + - fields: + - account + - account_city + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+name+stage.79f9642c.create.yaml + found: contracts/create_opportunity/account+account_city+name+stage.79f9642c.found.yaml + - fields: + - account + - account_city + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+currency+stage.ab0ce4e4.create.yaml + found: contracts/create_opportunity/account+account_city+currency+stage.ab0ce4e4.found.yaml + - fields: + - account + - account_city + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+currency+name.37d28d7b.create.yaml + found: contracts/create_opportunity/account+account_city+currency+name.37d28d7b.found.yaml + - fields: + - account + - account_city + - close_date + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+stage.e2243a69.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+stage.e2243a69.found.yaml + - fields: + - account + - account_city + - close_date + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+name.7eba75e7.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+name.7eba75e7.found.yaml + - fields: + - account + - account_city + - close_date + - currency + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+currency.f4da1bc2.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+currency.f4da1bc2.found.yaml + - fields: + - account + - account_city + - amount + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+stage.ef3af8a2.create.yaml + found: contracts/create_opportunity/account+account_city+amount+stage.ef3af8a2.found.yaml + - fields: + - account + - account_city + - amount + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+name.898ff6d9.create.yaml + found: contracts/create_opportunity/account+account_city+amount+name.898ff6d9.found.yaml + - fields: + - account + - account_city + - amount + - currency + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+currency.54e9d8de.create.yaml + found: contracts/create_opportunity/account+account_city+amount+currency.54e9d8de.found.yaml + - fields: + - account + - account_city + - amount + - close_date + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date.08f5c040.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date.08f5c040.found.yaml + update_lead: + mode: update + main_table: lead + parameters: + - first_name + - last_name + - company + - field + - value + identity: + - company + - first_name + - last_name + lists: [] + dispatch: field + variants: + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: accountName + update: contracts/update_lead/accountName+company+field+first_name+last_name+value.f0188cc9.update.yaml + found: contracts/update_lead/accountName+company+field+first_name+last_name+value.f0188cc9.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: account_name + update: contracts/update_lead/account_name+company+field+first_name+last_name+value.112a6b3c.update.yaml + found: contracts/update_lead/account_name+company+field+first_name+last_name+value.112a6b3c.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: addressCity + update: contracts/update_lead/addressCity+company+field+first_name+last_name+value.2e349c1f.update.yaml + found: contracts/update_lead/addressCity+company+field+first_name+last_name+value.2e349c1f.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: addressCountry + update: contracts/update_lead/addressCountry+company+field+first_name+last_name+value.cf5a4b98.update.yaml + found: contracts/update_lead/addressCountry+company+field+first_name+last_name+value.cf5a4b98.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: addressPostalCode + update: contracts/update_lead/addressPostalCode+company+field+first_name+last_name+value.1b559d01.update.yaml + found: contracts/update_lead/addressPostalCode+company+field+first_name+last_name+value.1b559d01.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: addressState + update: contracts/update_lead/addressState+company+field+first_name+last_name+value.d8fcca02.update.yaml + found: contracts/update_lead/addressState+company+field+first_name+last_name+value.d8fcca02.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: addressStreet + update: contracts/update_lead/addressStreet+company+field+first_name+last_name+value.ccdba0fe.update.yaml + found: contracts/update_lead/addressStreet+company+field+first_name+last_name+value.ccdba0fe.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: address_city + update: contracts/update_lead/address_city+company+field+first_name+last_name+value.39516a5c.update.yaml + found: contracts/update_lead/address_city+company+field+first_name+last_name+value.39516a5c.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: address_country + update: contracts/update_lead/address_country+company+field+first_name+last_name+value.26370712.update.yaml + found: contracts/update_lead/address_country+company+field+first_name+last_name+value.26370712.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: address_postal_code + update: contracts/update_lead/address_postal_code+company+field+first_name+last_name+value.b15ffc5b.update.yaml + found: contracts/update_lead/address_postal_code+company+field+first_name+last_name+value.b15ffc5b.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: address_state + update: contracts/update_lead/address_state+company+field+first_name+last_name+value.787f1586.update.yaml + found: contracts/update_lead/address_state+company+field+first_name+last_name+value.787f1586.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: address_street + update: contracts/update_lead/address_street+company+field+first_name+last_name+value.abad9297.update.yaml + found: contracts/update_lead/address_street+company+field+first_name+last_name+value.abad9297.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: assignedUserId + update: contracts/update_lead/assignedUserId+company+field+first_name+last_name+value.d92ef246.update.yaml + found: contracts/update_lead/assignedUserId+company+field+first_name+last_name+value.d92ef246.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: assigned_user_id + update: contracts/update_lead/assigned_user_id+company+field+first_name+last_name+value.e65072f2.update.yaml + found: contracts/update_lead/assigned_user_id+company+field+first_name+last_name+value.e65072f2.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: campaignId + update: contracts/update_lead/campaignId+company+field+first_name+last_name+value.5d7c3f99.update.yaml + found: contracts/update_lead/campaignId+company+field+first_name+last_name+value.5d7c3f99.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: campaign_id + update: contracts/update_lead/campaign_id+company+field+first_name+last_name+value.2ba0bedb.update.yaml + found: contracts/update_lead/campaign_id+company+field+first_name+last_name+value.2ba0bedb.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: createdAccountId + update: contracts/update_lead/createdAccountId+company+field+first_name+last_name+value.1e11a836.update.yaml + found: contracts/update_lead/createdAccountId+company+field+first_name+last_name+value.1e11a836.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: createdById + update: contracts/update_lead/createdById+company+field+first_name+last_name+value.ce7b1b24.update.yaml + found: contracts/update_lead/createdById+company+field+first_name+last_name+value.ce7b1b24.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: createdContactId + update: contracts/update_lead/createdContactId+company+field+first_name+last_name+value.5e8f2aec.update.yaml + found: contracts/update_lead/createdContactId+company+field+first_name+last_name+value.5e8f2aec.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: createdOpportunityId + update: contracts/update_lead/createdOpportunityId+company+field+first_name+last_name+value.57ce21be.update.yaml + found: contracts/update_lead/createdOpportunityId+company+field+first_name+last_name+value.57ce21be.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: created_account_id + update: contracts/update_lead/created_account_id+company+field+first_name+last_name+value.61ccbd38.update.yaml + found: contracts/update_lead/created_account_id+company+field+first_name+last_name+value.61ccbd38.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: created_by_id + update: contracts/update_lead/created_by_id+company+field+first_name+last_name+value.afd73f3f.update.yaml + found: contracts/update_lead/created_by_id+company+field+first_name+last_name+value.afd73f3f.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: created_contact_id + update: contracts/update_lead/created_contact_id+company+field+first_name+last_name+value.46f293a5.update.yaml + found: contracts/update_lead/created_contact_id+company+field+first_name+last_name+value.46f293a5.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: created_opportunity_id + update: contracts/update_lead/created_opportunity_id+company+field+first_name+last_name+value.45f20089.update.yaml + found: contracts/update_lead/created_opportunity_id+company+field+first_name+last_name+value.45f20089.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: description + update: contracts/update_lead/description+company+field+first_name+last_name+value.f7f378eb.update.yaml + found: contracts/update_lead/description+company+field+first_name+last_name+value.f7f378eb.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: email + update: contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml + found: contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: firstName + update: contracts/update_lead/firstName+company+field+first_name+last_name+value.df020818.update.yaml + found: contracts/update_lead/firstName+company+field+first_name+last_name+value.df020818.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: first_name + update: contracts/update_lead/first_name+company+field+first_name+last_name+value.fe20eac3.update.yaml + found: contracts/update_lead/first_name+company+field+first_name+last_name+value.fe20eac3.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: industry + update: contracts/update_lead/industry+company+field+first_name+last_name+value.681c4e87.update.yaml + found: contracts/update_lead/industry+company+field+first_name+last_name+value.681c4e87.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: lastName + update: contracts/update_lead/lastName+company+field+first_name+last_name+value.20ff2853.update.yaml + found: contracts/update_lead/lastName+company+field+first_name+last_name+value.20ff2853.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: last_name + update: contracts/update_lead/last_name+company+field+first_name+last_name+value.aa922c91.update.yaml + found: contracts/update_lead/last_name+company+field+first_name+last_name+value.aa922c91.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: middleName + update: contracts/update_lead/middleName+company+field+first_name+last_name+value.e12f42a6.update.yaml + found: contracts/update_lead/middleName+company+field+first_name+last_name+value.e12f42a6.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: middle_name + update: contracts/update_lead/middle_name+company+field+first_name+last_name+value.95efddfe.update.yaml + found: contracts/update_lead/middle_name+company+field+first_name+last_name+value.95efddfe.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: modifiedById + update: contracts/update_lead/modifiedById+company+field+first_name+last_name+value.1cbe39b1.update.yaml + found: contracts/update_lead/modifiedById+company+field+first_name+last_name+value.1cbe39b1.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: modified_by_id + update: contracts/update_lead/modified_by_id+company+field+first_name+last_name+value.f3a3d0bb.update.yaml + found: contracts/update_lead/modified_by_id+company+field+first_name+last_name+value.f3a3d0bb.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: opportunityAmountCurrency + update: contracts/update_lead/opportunityAmountCurrency+company+field+first_name+last_name+value.fe84c11f.update.yaml + found: contracts/update_lead/opportunityAmountCurrency+company+field+first_name+last_name+value.fe84c11f.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: opportunity_amount_currency + update: contracts/update_lead/opportunity_amount_currency+company+field+first_name+last_name+value.b0a33e68.update.yaml + found: contracts/update_lead/opportunity_amount_currency+company+field+first_name+last_name+value.b0a33e68.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: salutationName + update: contracts/update_lead/salutationName+company+field+first_name+last_name+value.f6ad2278.update.yaml + found: contracts/update_lead/salutationName+company+field+first_name+last_name+value.f6ad2278.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: salutation_name + update: contracts/update_lead/salutation_name+company+field+first_name+last_name+value.f8404b60.update.yaml + found: contracts/update_lead/salutation_name+company+field+first_name+last_name+value.f8404b60.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: source + update: contracts/update_lead/source+company+field+first_name+last_name+value.e5ca3051.update.yaml + found: contracts/update_lead/source+company+field+first_name+last_name+value.e5ca3051.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: status + update: contracts/update_lead/status+company+field+first_name+last_name+value.d77469a2.update.yaml + found: contracts/update_lead/status+company+field+first_name+last_name+value.d77469a2.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: title + update: contracts/update_lead/title+company+field+first_name+last_name+value.39d15090.update.yaml + found: contracts/update_lead/title+company+field+first_name+last_name+value.39d15090.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: website + update: contracts/update_lead/website+company+field+first_name+last_name+value.fdf2495b.update.yaml + found: contracts/update_lead/website+company+field+first_name+last_name+value.fdf2495b.found.yaml + log_call: + mode: create + main_table: call + parameters: + - subject + - status + - direction + - date_start + - duration_minutes + - account + - account_city + - contact + - lead + - lead_company + - description + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - account + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status+subject.1c40818d.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status+subject.1c40818d.found.yaml + - fields: + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status+su.a1e6dea7.create.yaml + found: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status+su.a1e6dea7.found.yaml + - fields: + - account + - contact + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status+subject.ac259c2d.create.yaml + found: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status+subject.ac259c2d.found.yaml + - fields: + - account + - account_city + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status+su.40bb5ef5.create.yaml + found: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status+su.40bb5ef5.found.yaml + - fields: + - account + - account_city + - contact + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status+subje.13cace1e.create.yaml + found: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status+subje.13cace1e.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status+subjec.6fa3c615.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status+subjec.6fa3c615.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status+subj.375289e9.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status+subj.375289e9.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.f9fb0e0d.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.f9fb0e0d.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.fa682ac0.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.fa682ac0.found.yaml + - fields: + - direction + - duration_minutes + - lead + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/direction+duration_minutes+lead+lead_company+status+subject.582a1975.create.yaml + found: contracts/log_call/direction+duration_minutes+lead+lead_company+status+subject.582a1975.found.yaml + - fields: + - date_start + - duration_minutes + - lead + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+duration_minutes+lead+lead_company+status+subject.cd43c2c6.create.yaml + found: contracts/log_call/date_start+duration_minutes+lead+lead_company+status+subject.cd43c2c6.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead_company+status+subject.d1a9d93e.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead_company+status+subject.d1a9d93e.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+status+subject.e3f86e48.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+status+subject.e3f86e48.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - lead_company + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+subject.7cd2c97e.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+subject.7cd2c97e.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - lead_company + - status + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status.fe5bab1b.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status.fe5bab1b.found.yaml + - fields: + - contact + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/contact+date_start+description+direction+duration_minutes+status+subject.ecb41244.create.yaml + found: contracts/log_call/contact+date_start+description+direction+duration_minutes+status+subject.ecb41244.found.yaml + - fields: + - account_city + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+date_start+description+direction+duration_minutes+status+subject.72b09e7c.create.yaml + found: contracts/log_call/account_city+date_start+description+direction+duration_minutes+status+subject.72b09e7c.found.yaml + - fields: + - account_city + - contact + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+description+direction+duration_minutes+status+subject.6027050a.create.yaml + found: contracts/log_call/account_city+contact+description+direction+duration_minutes+status+subject.6027050a.found.yaml + - fields: + - account_city + - contact + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+date_start+direction+duration_minutes+status+subject.fe2cf091.create.yaml + found: contracts/log_call/account_city+contact+date_start+direction+duration_minutes+status+subject.fe2cf091.found.yaml + - fields: + - account_city + - contact + - date_start + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+date_start+description+duration_minutes+status+subject.933068cc.create.yaml + found: contracts/log_call/account_city+contact+date_start+description+duration_minutes+status+subject.933068cc.found.yaml + - fields: + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+subject.53ca7468.create.yaml + found: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+subject.53ca7468.found.yaml + - fields: + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status.502d3047.create.yaml + found: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status.502d3047.found.yaml + - fields: + - account + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+date_start+description+direction+duration_minutes+status+subject.461a3bbb.create.yaml + found: contracts/log_call/account+date_start+description+direction+duration_minutes+status+subject.461a3bbb.found.yaml + - fields: + - account + - contact + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+description+direction+duration_minutes+status+subject.4eeb71c8.create.yaml + found: contracts/log_call/account+contact+description+direction+duration_minutes+status+subject.4eeb71c8.found.yaml + - fields: + - account + - contact + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+date_start+direction+duration_minutes+status+subject.88c05db0.create.yaml + found: contracts/log_call/account+contact+date_start+direction+duration_minutes+status+subject.88c05db0.found.yaml + - fields: + - account + - contact + - date_start + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+date_start+description+duration_minutes+status+subject.92c40c7a.create.yaml + found: contracts/log_call/account+contact+date_start+description+duration_minutes+status+subject.92c40c7a.found.yaml + - fields: + - account + - contact + - date_start + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+subject.36ca60f2.create.yaml + found: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+subject.36ca60f2.found.yaml + - fields: + - account + - contact + - date_start + - description + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status.f18701ec.create.yaml + found: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status.f18701ec.found.yaml + - fields: + - account + - account_city + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+description+direction+duration_minutes+status+subject.2e513d2e.create.yaml + found: contracts/log_call/account+account_city+description+direction+duration_minutes+status+subject.2e513d2e.found.yaml + - fields: + - account + - account_city + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+date_start+direction+duration_minutes+status+subject.5dd1577d.create.yaml + found: contracts/log_call/account+account_city+date_start+direction+duration_minutes+status+subject.5dd1577d.found.yaml + - fields: + - account + - account_city + - date_start + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+date_start+description+duration_minutes+status+subject.c2f5c493.create.yaml + found: contracts/log_call/account+account_city+date_start+description+duration_minutes+status+subject.c2f5c493.found.yaml + - fields: + - account + - account_city + - date_start + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+subject.1444168f.create.yaml + found: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+subject.1444168f.found.yaml + - fields: + - account + - account_city + - date_start + - description + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status.6b8a69ef.create.yaml + found: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status.6b8a69ef.found.yaml + - fields: + - account + - account_city + - contact + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+direction+duration_minutes+status+subject.25d5283f.create.yaml + found: contracts/log_call/account+account_city+contact+direction+duration_minutes+status+subject.25d5283f.found.yaml + - fields: + - account + - account_city + - contact + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+description+duration_minutes+status+subject.994dc4c5.create.yaml + found: contracts/log_call/account+account_city+contact+description+duration_minutes+status+subject.994dc4c5.found.yaml + - fields: + - account + - account_city + - contact + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+subject.5213a5ad.create.yaml + found: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+subject.5213a5ad.found.yaml + - fields: + - account + - account_city + - contact + - description + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status.a5f10536.create.yaml + found: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status.a5f10536.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+duration_minutes+status+subject.a72c4dca.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+duration_minutes+status+subject.a72c4dca.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+subject.a261cf3e.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+subject.a261cf3e.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status.97f875f0.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status.97f875f0.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+subject.2740ed8a.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+subject.2740ed8a.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status.eec71d6e.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status.eec71d6e.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes.d2b14413.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes.d2b14413.found.yaml + - fields: + - duration_minutes + - lead + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/duration_minutes+lead+lead_company+status+subject.248320eb.create.yaml + found: contracts/log_call/duration_minutes+lead+lead_company+status+subject.248320eb.found.yaml + - fields: + - direction + - duration_minutes + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/direction+duration_minutes+lead_company+status+subject.31e67761.create.yaml + found: contracts/log_call/direction+duration_minutes+lead_company+status+subject.31e67761.found.yaml + - fields: + - direction + - duration_minutes + - lead + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/direction+duration_minutes+lead+status+subject.80c440d6.create.yaml + found: contracts/log_call/direction+duration_minutes+lead+status+subject.80c440d6.found.yaml + - fields: + - direction + - duration_minutes + - lead + - lead_company + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/direction+duration_minutes+lead+lead_company+subject.de5e153e.create.yaml + found: contracts/log_call/direction+duration_minutes+lead+lead_company+subject.de5e153e.found.yaml + - fields: + - direction + - duration_minutes + - lead + - lead_company + - status + lists: {} + dispatch_value: null + create: contracts/log_call/direction+duration_minutes+lead+lead_company+status.0ab0a0ff.create.yaml + found: contracts/log_call/direction+duration_minutes+lead+lead_company+status.0ab0a0ff.found.yaml + - fields: + - date_start + - duration_minutes + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+duration_minutes+lead_company+status+subject.88eff1f9.create.yaml + found: contracts/log_call/date_start+duration_minutes+lead_company+status+subject.88eff1f9.found.yaml + - fields: + - date_start + - duration_minutes + - lead + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+duration_minutes+lead+status+subject.1bf286d5.create.yaml + found: contracts/log_call/date_start+duration_minutes+lead+status+subject.1bf286d5.found.yaml + - fields: + - date_start + - duration_minutes + - lead + - lead_company + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+duration_minutes+lead+lead_company+subject.f23cce08.create.yaml + found: contracts/log_call/date_start+duration_minutes+lead+lead_company+subject.f23cce08.found.yaml + - fields: + - date_start + - duration_minutes + - lead + - lead_company + - status + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+duration_minutes+lead+lead_company+status.5a81f788.create.yaml + found: contracts/log_call/date_start+duration_minutes+lead+lead_company+status.5a81f788.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+status+subject.023b89f7.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+status+subject.023b89f7.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead_company + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead_company+subject.8cb30c88.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead_company+subject.8cb30c88.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead_company + - status + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead_company+status.93dd5488.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead_company+status.93dd5488.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+subject.8d8612d3.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+subject.8d8612d3.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - status + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+status.500ced17.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+status.500ced17.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - lead_company + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company.9c7e8372.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company.9c7e8372.found.yaml + - fields: + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+description+direction+duration_minutes+status+subject.2e2c7e7a.create.yaml + found: contracts/log_call/date_start+description+direction+duration_minutes+status+subject.2e2c7e7a.found.yaml + - fields: + - contact + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/contact+description+direction+duration_minutes+status+subject.47ddf296.create.yaml + found: contracts/log_call/contact+description+direction+duration_minutes+status+subject.47ddf296.found.yaml + - fields: + - contact + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/contact+date_start+direction+duration_minutes+status+subject.948cf7ee.create.yaml + found: contracts/log_call/contact+date_start+direction+duration_minutes+status+subject.948cf7ee.found.yaml + - fields: + - contact + - date_start + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/contact+date_start+description+duration_minutes+status+subject.9f7503e2.create.yaml + found: contracts/log_call/contact+date_start+description+duration_minutes+status+subject.9f7503e2.found.yaml + - fields: + - contact + - date_start + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/contact+date_start+description+direction+duration_minutes+subject.9f27d6b5.create.yaml + found: contracts/log_call/contact+date_start+description+direction+duration_minutes+subject.9f27d6b5.found.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/report.json b/packages/flow/benchmark/generate_contracts/results/espocrm/report.json new file mode 100644 index 0000000000..a591f68f12 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/report.json @@ -0,0 +1,170 @@ +{ + "applied": [], + "dialect": "mysql", + "tables": 141, + "questions": [ + { + "number": 1, + "key": "bookkeeping", + "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): call_user (1 per write), note (1 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change): action_history_record, auth_log_record, note, note_user Columns that change on their own and are not compared: account: stream_updated_at; contact: stream_updated_at; lead: modified_at, modified_by_id, stream_updated_at Columns of a new record that differ between demonstrations and are not checked: call: parent_type, uid; opportunity: last_stage, probability Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", + "proposal": "accept", + "answered": true + }, + { + "number": 2, + "key": "recheck_after_s", + "text": "How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `.", + "proposal": 300, + "answered": true + } + ], + "kinds": { + "create_contact": { + "mode": "create", + "main_table": "contact", + "locations": { + "account": "contact.account_id -> account.name (exact)", + "account_city": "contact.account_id -> account.billing_address_city (exact)", + "email": "entity_email_address (whose entity_id is the contact key).email_address_id -> email_address.name (exact)", + "first_name": "contact.first_name (exact)", + "last_name": "contact.last_name (exact)", + "phone": "entity_phone_number (whose entity_id is the contact key).phone_number_id -> phone_number.name (phone)", + "title": "account_contact (whose contact_id is the contact key).role (exact)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": { + "account#2": "account_contact", + "account_city#2": "account_contact", + "email": "entity_email_address", + "phone": "entity_phone_number", + "title": "account_contact" + }, + "dispatch_children": {}, + "lists": {}, + "side_rows": [ + { + "table": "note", + "count": 1, + "unless": null + } + ], + "logs": [ + "action_history_record", + "auth_log_record" + ], + "refused": [], + "unresolved": {}, + "emitted": 64, + "rejected": [] + }, + "create_opportunity": { + "mode": "create", + "main_table": "opportunity", + "locations": { + "account": "opportunity.account_id -> account.name (exact)", + "account_city": "opportunity.account_id -> account.billing_address_city (exact)", + "amount": "opportunity.amount (number)", + "close_date": "opportunity.close_date (date)", + "currency": "opportunity.amount_currency (exact)", + "name": "opportunity.name (exact)", + "stage": "opportunity.stage (exact)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": {}, + "dispatch_children": {}, + "lists": {}, + "side_rows": [ + { + "table": "note", + "count": 1, + "unless": null + } + ], + "logs": [ + "action_history_record", + "auth_log_record" + ], + "refused": [], + "unresolved": {}, + "emitted": 64, + "rejected": [] + }, + "update_lead": { + "mode": "update", + "main_table": "lead", + "locations": { + "company": "lead.account_name (exact)", + "first_name": "lead.first_name (exact)", + "last_name": "lead.last_name (exact)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": {}, + "dispatch_children": { + "email": { + "value@email": "entity_email_address" + } + }, + "lists": {}, + "side_rows": [], + "logs": [ + "action_history_record", + "auth_log_record", + "note" + ], + "refused": [], + "unresolved": {}, + "emitted": 43, + "rejected": [] + }, + "log_call": { + "mode": "create", + "main_table": "call", + "locations": { + "account": "call.account_id -> account.name (exact)", + "account_city": "call.account_id -> account.billing_address_city (exact)", + "contact": "call_contact (whose call_id is the call key).contact_id -> contact.first_name || ' ' || last_name (exact)", + "date_start": "call.date_start (datetime)", + "description": "call.description (exact)", + "direction": "call.direction (exact)", + "duration_minutes": "call.date_start || ' ' || date_end (minutes)", + "lead": "call.parent_id -> lead.first_name || ' ' || last_name (exact)", + "lead_company": "call.parent_id -> lead.account_name (exact)", + "status": "call.status (exact)", + "subject": "call.name (exact)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": { + "contact": "call_contact", + "lead#2": "call_lead", + "lead_company#2": "call_lead" + }, + "dispatch_children": {}, + "lists": {}, + "side_rows": [ + { + "table": "call_user", + "count": 1, + "unless": null + } + ], + "logs": [ + "action_history_record", + "auth_log_record", + "note", + "note_user" + ], + "refused": [], + "unresolved": {}, + "emitted": 64, + "rejected": [] + } + } +} \ No newline at end of file From baf86e78a42264e84b6fe376e616f840c80c027e Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 17:46:26 -0400 Subject: [PATCH 32/94] Versions from an audit trail: narrow only by the record's type --- .../flow/openadapt_flow/generate/verify_contract.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 43bd614da2..904491f9f7 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -971,17 +971,20 @@ def _audit_source( if not pairs or len(pairs) != 1: continue lc, ac, param = next(iter(pairs)) - # Rows of the same type: their constant text and whole-number columns. + # Only a column naming the record's type (``target_type: Lead``) + # narrows the read. Anything else the demonstrations had in common + # (the action, the client address) could hide another user's save, + # so it isn't used: an entry by anyone else stops the write. where = {} + names = {plan.anchor.lower(), plan.anchor.lower().split("_")[-1]} for col in table.columns: - if col.name in (lc, ac) or col.name in table.primary_key: + if col.name in (lc, ac) or col.kind != "text": continue values = {r.get(col.name) for r in rows} if len(values) == 1: v = next(iter(values)) - if v is not None and (col.kind == "text" or _WHOLE_NUMBER.match(v)): - if "\\" not in v and "'" not in v and "%" not in v: - where[col.name] = v + if v is not None and v.lower() in names and v.isidentifier(): + where[col.name] = v found = { "kind": "audit", "table": name, From 6ed3c6c02c7b96f58d1d023c1a10f0549c587da8 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 17:46:52 -0400 Subject: [PATCH 33/94] docs: lost-update versions in generated contracts --- packages/flow/docs/GENERATE_CONTRACTS.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 0b6d28a41c..6013304c66 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -204,6 +204,17 @@ reads, with every column. On PostgreSQL, `frame.database` isn't available, so every other table is read as its own surface; tables proposed as logs are not read there. +**Lost updates.** An update's contracts set `frame.versions`, so another +user's save to the target inside the window stops the write. The generator +takes the versions from the app's own audit trail when every update +demonstration added a row there that names the record and the acting user +(Redmine's journals, EspoCRM's `action_history_record`, Dolibarr's agenda +events); the read is narrowed only by a column naming the record's type, +so any other user's entry for the record counts. Otherwise it uses a whole +number column of the record that went up by one in every demonstration +(Redmine's `lock_version`). With neither, the update has no version check, +and `report.json` shows `versions: null`. + **The recheck window.** On MySQL and MariaDB, one question asks how long the app can still write on its own after a verified write. The answer is `frame.database.recheck_after_s`; the proposal is 300 seconds. From c51437ba62751ede846cdbeb0221ec924d80b3e9 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 18:11:19 -0400 Subject: [PATCH 34/94] Versions: a column named as a version; the author column when several hold the actor --- ...last_name+phone+title.fef035cb.create.yaml | 339 ++++++++++++++++++ ...+last_name+phone+title.fef035cb.found.yaml | 196 ++++++++++ ...e+currency+name+stage.1d2f2376.create.yaml | 148 ++++++++ ...te+currency+name+stage.1d2f2376.found.yaml | 87 +++++ ...on+duration_minutes+s.dca67171.create.yaml | 175 +++++++++ ...ion+duration_minutes+s.dca67171.found.yaml | 135 +++++++ ...t_name+last_name+value.a43b40a4.found.yaml | 113 ++++++ ..._name+last_name+value.a43b40a4.update.yaml | 160 +++++++++ .../results/espocrm/report.json | 15 +- .../generate/verify_contract.py | 21 +- 10 files changed, 1385 insertions(+), 4 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml new file mode 100644 index 0000000000..0a79c06781 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml @@ -0,0 +1,339 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_contact, create, fields account, account_city, email, first_name, last_name, phone, title. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` + AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` + AS CHAR) AS `last_name`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`do_not_call` + AS CHAR) AS `do_not_call`, CAST(t.`address_street` AS CHAR) AS `address_street`, CAST(t.`address_city` + AS CHAR) AS `address_city`, CAST(t.`address_state` AS CHAR) AS `address_state`, CAST(t.`address_country` + AS CHAR) AS `address_country`, CAST(t.`address_postal_code` AS CHAR) AS `address_postal_code`, + CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, + CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, + CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM + `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, + (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) + = CAST(t.`account_id` AS BINARY)) AS `p__account_city`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__deleted`, CASE WHEN t.`do_not_call` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__do_not_call` FROM `contact` t + rows_account_contact: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`contact_id` + AS CHAR) AS `contact_id`, CAST(t.`role` AS CHAR) AS `role`, CAST(t.`is_inactive` AS CHAR) AS `is_inactive`, + CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` + a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` a1 WHERE a1.`id` + = t.`contact_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` AS CHAR) + FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` + a1 WHERE a1.`id` = t.`contact_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` AS CHAR) + FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS + `p__account`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, + CASE WHEN t.`is_inactive` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__is_inactive`, + (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) + = CAST(t.`account_id` AS BINARY)) AS `p__account_city` FROM `account_contact` t + rows_entity_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` + AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS + BINARY)) AS `p__email`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' + END AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__primary` FROM `entity_email_address` t + rows_entity_phone_number: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`phone_number_id` + AS CHAR) AS `phone_number_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `phone_number` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`phone_number_id` AS BINARY)) + AS `p__phone`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, + CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__primary` FROM + `entity_phone_number` t + raw_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`lower` AS CHAR) AS `lower`, CAST(t.`invalid` AS CHAR) AS `invalid`, + CAST(t.`opt_out` AS CHAR) AS `opt_out` FROM `email_address` t + raw_phone_number: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`numeric` AS CHAR) AS `numeric`, + CAST(t.`invalid` AS CHAR) AS `invalid`, CAST(t.`opt_out` AS CHAR) AS `opt_out` FROM `phone_number` + t + raw_note: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`post` + AS CHAR) AS `post`, CAST(t.`data` AS CHAR) AS `data`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`target_type` + AS CHAR) AS `target_type`, CAST(t.`number` AS CHAR) AS `number`, CAST(t.`is_global` AS CHAR) AS + `is_global`, CAST(t.`is_internal` AS CHAR) AS `is_internal`, CAST(t.`is_pinned` AS CHAR) AS `is_pinned`, + CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, + CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` AS CHAR) AS `parent_type`, CAST(t.`related_id` + AS CHAR) AS `related_id`, CAST(t.`related_type` AS CHAR) AS `related_type`, CAST(t.`created_by_id` + AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`super_parent_id` + AS CHAR) AS `super_parent_id`, CAST(t.`super_parent_type` AS CHAR) AS `super_parent_type` FROM + `note` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + first_name: + param: first_name + last_name: + param: last_name + created_by_id: + param: bot_user_id + pin__deleted: + literal: '1' + pin__do_not_call: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds account + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__account: + param: account + pin__deleted: + literal: '1' + pin__is_inactive: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds account_city + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__account_city: + param: account_city + pin__deleted: + literal: '1' + pin__is_inactive: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_entity_email_address|a row beside the main record holds email + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__email: + param: email + kind: email + pin__deleted: + literal: '1' + entity_type: + literal: Contact + pin__primary: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_entity_phone_number|a row beside the main record holds phone + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__phone: + param: phone + kind: phone + pin__deleted: + literal: '1' + entity_type: + literal: Contact + pin__primary: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds title + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + role: + param: title + pin__deleted: + literal: '1' + pin__is_inactive: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_email_address|the new email_address row holds email + match: + name: + param: email + kind: email + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_phone_number|the new phone_number row holds phone + match: + name: + param: phone + kind: phone + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_note|1 row(s) the app adds to note + match: + created_by_id: + param: bot_user_id + deleted: + literal: '0' + is_global: + literal: '0' + is_internal: + literal: '0' + is_pinned: + literal: '0' + parent_type: + literal: Contact + super_parent_type: + literal: Account + type: + literal: Create + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: account_contact + - table: contact + - table: entity_email_address + - table: entity_phone_number + views: + email_address: raw_email_address + phone_number: raw_phone_number + note: raw_note + settle_s: 1 + settle_attempts: 5 + new_row_fields: + email_address: + - deleted + - id + - invalid + - lower + - opt_out + phone_number: + - deleted + - id + - invalid + - numeric + - opt_out + - type + note: + - created_at + - data + - id + - modified_at + - modified_by_id + - number + - parent_id + - post + - related_id + - related_type + - super_parent_id + - target_type + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml new file mode 100644 index 0000000000..5ac58e6a52 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml @@ -0,0 +1,196 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_contact, found, fields account, account_city, email, first_name, last_name, phone, title. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` + AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` + AS CHAR) AS `last_name`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`do_not_call` + AS CHAR) AS `do_not_call`, CAST(t.`address_street` AS CHAR) AS `address_street`, CAST(t.`address_city` + AS CHAR) AS `address_city`, CAST(t.`address_state` AS CHAR) AS `address_state`, CAST(t.`address_country` + AS CHAR) AS `address_country`, CAST(t.`address_postal_code` AS CHAR) AS `address_postal_code`, + CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, + CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, + CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM + `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, + (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) + = CAST(t.`account_id` AS BINARY)) AS `p__account_city` FROM `contact` t + rows_account_contact: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`contact_id` + AS CHAR) AS `contact_id`, CAST(t.`role` AS CHAR) AS `role`, CAST(t.`is_inactive` AS CHAR) AS `is_inactive`, + CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` + a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` a1 WHERE a1.`id` + = t.`contact_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` AS CHAR) + FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` + a1 WHERE a1.`id` = t.`contact_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` AS CHAR) + FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS + `p__account`, (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` + AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account_city` FROM `account_contact` t + rows_entity_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` + AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS + BINARY)) AS `p__email` FROM `entity_email_address` t + rows_entity_phone_number: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`phone_number_id` + AS CHAR) AS `phone_number_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `phone_number` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`phone_number_id` AS BINARY)) + AS `p__phone` FROM `entity_phone_number` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + first_name: + param: first_name + last_name: + param: last_name + expected_count: 1 +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds account + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__account: + param: account + expected_count: 1 +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds account_city + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__account_city: + param: account_city + expected_count: 1 +- kind: record_written + probe: surface=rows_entity_email_address|a row beside the main record holds email + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__email: + param: email + kind: email + expected_count: 1 +- kind: record_written + probe: surface=rows_entity_phone_number|a row beside the main record holds phone + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__phone: + param: phone + kind: phone + expected_count: 1 +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds title + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + role: + param: title + expected_count: 1 +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: account_contact + - table: contact + - table: entity_email_address + - table: entity_phone_number + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml new file mode 100644 index 0000000000..02c8105314 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml @@ -0,0 +1,148 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_opportunity, create, fields account, account_city, amount, close_date, currency, name, stage. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`amount` AS CHAR) AS `amount`, CAST(t.`stage` AS CHAR) AS `stage`, + CAST(t.`last_stage` AS CHAR) AS `last_stage`, CAST(t.`probability` AS CHAR) AS `probability`, + CAST(t.`lead_source` AS CHAR) AS `lead_source`, CAST(t.`close_date` AS CHAR) AS `close_date`, + CAST(t.`description` AS CHAR) AS `description`, CAST(t.`created_at` AS CHAR) AS `created_at`, + CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`amount_currency` AS CHAR) AS `amount_currency`, + CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`account_id` AS CHAR) AS `account_id`, + CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` AS CHAR) AS + `version_number`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) + = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` AS CHAR) + FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account_city`, + CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`amount` = CAST(%(v0)s AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__amount`, CASE WHEN t.`close_date` = CAST(%(v1)s AS + DATE) THEN '1' ELSE '0' END AS `p__close_date`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__deleted`, CASE WHEN t.`version_number` = (SELECT s.`version_number` + FROM `account` s WHERE s.`id` = t.`account_id`) THEN '1' ELSE '0' END AS `pin__version_number` + FROM `opportunity` t + sql_query_params: + v0: + param: amount + v1: + param: close_date + raw_note: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`post` + AS CHAR) AS `post`, CAST(t.`data` AS CHAR) AS `data`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`target_type` + AS CHAR) AS `target_type`, CAST(t.`number` AS CHAR) AS `number`, CAST(t.`is_global` AS CHAR) AS + `is_global`, CAST(t.`is_internal` AS CHAR) AS `is_internal`, CAST(t.`is_pinned` AS CHAR) AS `is_pinned`, + CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, + CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` AS CHAR) AS `parent_type`, CAST(t.`related_id` + AS CHAR) AS `related_id`, CAST(t.`related_type` AS CHAR) AS `related_type`, CAST(t.`created_by_id` + AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`super_parent_id` + AS CHAR) AS `super_parent_id`, CAST(t.`super_parent_type` AS CHAR) AS `super_parent_type` FROM + `note` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + p__amount: + literal: '1' + p__close_date: + literal: '1' + amount_currency: + param: currency + name: + param: name + stage: + param: stage + created_by_id: + param: bot_user_id + pin__deleted: + literal: '1' + pin__version_number: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_note|1 row(s) the app adds to note + match: + created_by_id: + param: bot_user_id + deleted: + literal: '0' + is_global: + literal: '0' + is_internal: + literal: '0' + is_pinned: + literal: '0' + parent_type: + literal: Opportunity + super_parent_type: + literal: Account + type: + literal: Create + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: opportunity + views: + note: raw_note + settle_s: 1 + settle_attempts: 5 + new_row_fields: + note: + - created_at + - data + - id + - modified_at + - modified_by_id + - number + - parent_id + - post + - related_id + - related_type + - super_parent_id + - target_type + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml new file mode 100644 index 0000000000..1942899ebc --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml @@ -0,0 +1,87 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_opportunity, found, fields account, account_city, amount, close_date, currency, name, stage. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`amount` AS CHAR) AS `amount`, CAST(t.`stage` AS CHAR) AS `stage`, + CAST(t.`last_stage` AS CHAR) AS `last_stage`, CAST(t.`probability` AS CHAR) AS `probability`, + CAST(t.`lead_source` AS CHAR) AS `lead_source`, CAST(t.`close_date` AS CHAR) AS `close_date`, + CAST(t.`description` AS CHAR) AS `description`, CAST(t.`created_at` AS CHAR) AS `created_at`, + CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`amount_currency` AS CHAR) AS `amount_currency`, + CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`account_id` AS CHAR) AS `account_id`, + CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` AS CHAR) AS + `version_number`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) + = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` AS CHAR) + FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account_city`, + CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`amount` = CAST(%(v0)s AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__amount`, CASE WHEN t.`close_date` = CAST(%(v1)s AS + DATE) THEN '1' ELSE '0' END AS `p__close_date` FROM `opportunity` t + sql_query_params: + v0: + param: amount + v1: + param: close_date +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + p__amount: + literal: '1' + p__close_date: + literal: '1' + amount_currency: + param: currency + name: + param: name + stage: + param: stage + expected_count: 1 +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: opportunity + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml new file mode 100644 index 0000000000..b17aa26683 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml @@ -0,0 +1,175 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_call, create, fields account, account_city, contact, date_start, description, direction, duration_minutes, status, subject. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`date_start` AS CHAR) AS `date_start`, + CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`direction` AS CHAR) AS `direction`, CAST(t.`description` + AS CHAR) AS `description`, CAST(t.`uid` AS CHAR) AS `uid`, CAST(t.`created_at` AS CHAR) AS `created_at`, + CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` + AS CHAR) AS `parent_type`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`created_by_id` + AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` + AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` + AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` + AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS + `p__account_city`, CASE WHEN t.`date_start` = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS + `p__date_start`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (TIMESTAMPDIFF(SECOND, + t.`date_start`, t.`date_end`) / 60) = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `p__duration_minutes`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__deleted` FROM `call` t + sql_query_params: + v0: &id002 + param: date_start + v1: &id003 + param: duration_minutes + rows_call_contact: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`call_id` AS CHAR) AS `call_id`, CAST(t.`contact_id` + AS CHAR) AS `contact_id`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`deleted` AS CHAR) AS `deleted`, + (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` + AS BINARY)) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account_city`, CASE WHEN (SELECT a1.`date_start` + FROM `call` a1 WHERE a1.`id` = t.`call_id`) = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS + `main__date_start`, (SELECT CAST(a1.`description` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + AS `main__description`, (SELECT CAST(a1.`direction` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + AS `main__direction`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT + (TIMESTAMPDIFF(SECOND, a1.`date_start`, a1.`date_end`) / 60) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__duration_minutes`, (SELECT CAST(a1.`status` + AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__status`, (SELECT CAST(a1.`name` + AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__subject`, (SELECT CONCAT(CAST(a1.`first_name` + AS CHAR), ' ', CAST(a1.`last_name` AS CHAR)) FROM `contact` a1 WHERE CAST(a1.`id` AS BINARY) = + CAST(t.`contact_id` AS BINARY)) AS `p__contact`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__deleted` FROM `call_contact` t + sql_query_params: + v0: *id002 + v1: *id003 + raw_call_user: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`user_id` AS CHAR) AS `user_id`, CAST(t.`call_id` + AS CHAR) AS `call_id`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`deleted` AS CHAR) AS `deleted` + FROM `call_user` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + p__date_start: + literal: '1' + description: + param: description + direction: + param: direction + p__duration_minutes: + literal: '1' + status: + param: status + name: + param: subject + assigned_user_id: + param: bot_user_id + created_by_id: + param: bot_user_id + pin__deleted: + literal: '1' + parent_type: + literal: Account + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_call_contact|a row beside the main record holds contact + match: + main__account: + param: account + main__account_city: + param: account_city + main__date_start: + literal: '1' + main__description: + param: description + main__direction: + param: direction + main__duration_minutes: + literal: '1' + main__status: + param: status + main__subject: + param: subject + p__contact: + param: contact + pin__deleted: + literal: '1' + status: + literal: None + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_call_user|1 row(s) the app adds to call_user + match: + deleted: + literal: '0' + status: + literal: Accepted + user_id: + param: bot_user_id + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - modified_at + - modified_by_id + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: note + - table: note_user + - table: call + - table: call_contact + views: + call_user: raw_call_user + settle_s: 1 + settle_attempts: 5 + new_row_fields: + call_user: + - call_id + - id + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml new file mode 100644 index 0000000000..6f722304fc --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml @@ -0,0 +1,135 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_call, found, fields account, account_city, contact, date_start, description, direction, duration_minutes, status, subject. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`date_start` AS CHAR) AS `date_start`, + CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`direction` AS CHAR) AS `direction`, CAST(t.`description` + AS CHAR) AS `description`, CAST(t.`uid` AS CHAR) AS `uid`, CAST(t.`created_at` AS CHAR) AS `created_at`, + CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` + AS CHAR) AS `parent_type`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`created_by_id` + AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` + AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` + AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` + AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS + `p__account_city`, CASE WHEN t.`date_start` = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS + `p__date_start`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (TIMESTAMPDIFF(SECOND, + t.`date_start`, t.`date_end`) / 60) = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `p__duration_minutes` FROM `call` t + sql_query_params: + v0: &id002 + param: date_start + v1: &id003 + param: duration_minutes + rows_call_contact: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`call_id` AS CHAR) AS `call_id`, CAST(t.`contact_id` + AS CHAR) AS `contact_id`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`deleted` AS CHAR) AS `deleted`, + (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` + AS BINARY)) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account_city`, CASE WHEN (SELECT a1.`date_start` + FROM `call` a1 WHERE a1.`id` = t.`call_id`) = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS + `main__date_start`, (SELECT CAST(a1.`description` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + AS `main__description`, (SELECT CAST(a1.`direction` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + AS `main__direction`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT + (TIMESTAMPDIFF(SECOND, a1.`date_start`, a1.`date_end`) / 60) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__duration_minutes`, (SELECT CAST(a1.`status` + AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__status`, (SELECT CAST(a1.`name` + AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__subject`, (SELECT CONCAT(CAST(a1.`first_name` + AS CHAR), ' ', CAST(a1.`last_name` AS CHAR)) FROM `contact` a1 WHERE CAST(a1.`id` AS BINARY) = + CAST(t.`contact_id` AS BINARY)) AS `p__contact` FROM `call_contact` t + sql_query_params: + v0: *id002 + v1: *id003 +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + p__date_start: + literal: '1' + description: + param: description + direction: + param: direction + p__duration_minutes: + literal: '1' + status: + param: status + name: + param: subject + expected_count: 1 +- kind: record_written + probe: surface=rows_call_contact|a row beside the main record holds contact + match: + main__account: + param: account + main__account_city: + param: account_city + main__date_start: + literal: '1' + main__description: + param: description + main__direction: + param: direction + main__duration_minutes: + literal: '1' + main__status: + param: status + main__subject: + param: subject + p__contact: + param: contact + expected_count: 1 +frame: + ignore_fields: + - modified_at + - modified_by_id + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: note + - table: note_user + - table: call + - table: call_contact + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml new file mode 100644 index 0000000000..0d63d283c1 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml @@ -0,0 +1,113 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_lead, found, fields company, field, first_name, last_name, value. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` + AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` + AS CHAR) AS `last_name`, CAST(t.`title` AS CHAR) AS `title`, CAST(t.`status` AS CHAR) AS `status`, + CAST(t.`source` AS CHAR) AS `source`, CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`opportunity_amount` + AS CHAR) AS `opportunity_amount`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`address_street` + AS CHAR) AS `address_street`, CAST(t.`address_city` AS CHAR) AS `address_city`, CAST(t.`address_state` + AS CHAR) AS `address_state`, CAST(t.`address_country` AS CHAR) AS `address_country`, CAST(t.`address_postal_code` + AS CHAR) AS `address_postal_code`, CAST(t.`do_not_call` AS CHAR) AS `do_not_call`, CAST(t.`description` + AS CHAR) AS `description`, CAST(t.`converted_at` AS CHAR) AS `converted_at`, CAST(t.`created_at` + AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`account_name` + AS CHAR) AS `account_name`, CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`opportunity_amount_currency` + AS CHAR) AS `opportunity_amount_currency`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_account_id` AS CHAR) AS `created_account_id`, CAST(t.`created_contact_id` AS CHAR) + AS `created_contact_id`, CAST(t.`created_opportunity_id` AS CHAR) AS `created_opportunity_id` + FROM `lead` t + rows_entity_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` + AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT CAST(a1.`account_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__company`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS + BINARY)) AS `p__value` FROM `entity_email_address` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + account_name: + param: company + first_name: + param: first_name + last_name: + param: last_name + expected_count: 1 +- kind: record_written + probe: surface=rows_entity_email_address|a row beside the main record holds value + match: + main__company: + param: company + main__first_name: + param: first_name + main__last_name: + param: last_name + p__value: + param: value + kind: email + expected_count: 1 +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + versions: + read: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`target_id` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`user_id` + AS CHAR) AS `author` FROM `action_history_record` t WHERE CAST(t.`target_type` AS CHAR) = 'Lead' + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: note + - table: entity_email_address + - table: lead + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + account: + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml new file mode 100644 index 0000000000..8063d931de --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml @@ -0,0 +1,160 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_lead, update, fields company, field, first_name, last_name, value. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` + AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` + AS CHAR) AS `last_name`, CAST(t.`title` AS CHAR) AS `title`, CAST(t.`status` AS CHAR) AS `status`, + CAST(t.`source` AS CHAR) AS `source`, CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`opportunity_amount` + AS CHAR) AS `opportunity_amount`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`address_street` + AS CHAR) AS `address_street`, CAST(t.`address_city` AS CHAR) AS `address_city`, CAST(t.`address_state` + AS CHAR) AS `address_state`, CAST(t.`address_country` AS CHAR) AS `address_country`, CAST(t.`address_postal_code` + AS CHAR) AS `address_postal_code`, CAST(t.`do_not_call` AS CHAR) AS `do_not_call`, CAST(t.`description` + AS CHAR) AS `description`, CAST(t.`converted_at` AS CHAR) AS `converted_at`, CAST(t.`created_at` + AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`account_name` + AS CHAR) AS `account_name`, CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`opportunity_amount_currency` + AS CHAR) AS `opportunity_amount_currency`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_account_id` AS CHAR) AS `created_account_id`, CAST(t.`created_contact_id` AS CHAR) + AS `created_contact_id`, CAST(t.`created_opportunity_id` AS CHAR) AS `created_opportunity_id` + FROM `lead` t + rows_entity_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` + AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT CAST(a1.`account_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__company`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS + BINARY)) AS `p__value`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' + END AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__primary` FROM `entity_email_address` t + raw_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`lower` AS CHAR) AS `lower`, CAST(t.`invalid` AS CHAR) AS `invalid`, + CAST(t.`opt_out` AS CHAR) AS `opt_out` FROM `email_address` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + account_name: + param: company + first_name: + param: first_name + last_name: + param: last_name + expected_count: 1 +- kind: record_written + probe: surface=rows_entity_email_address|a row beside the main record holds value + match: + main__company: &id002 + param: company + main__first_name: &id003 + param: first_name + main__last_name: &id004 + param: last_name + p__value: + param: value + kind: email + pin__deleted: &id005 + literal: '1' + entity_type: &id006 + literal: Lead + pin__primary: &id007 + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_entity_email_address|the record's rows here after the write + match: + main__company: *id002 + main__first_name: *id003 + main__last_name: *id004 + pin__deleted: *id005 + entity_type: *id006 + pin__primary: *id007 + expected_count: 1 +- kind: record_written + probe: surface=raw_email_address|the new email_address row holds value + match: + name: + param: value + kind: email + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + versions: + read: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`target_id` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`user_id` + AS CHAR) AS `author` FROM `action_history_record` t WHERE CAST(t.`target_type` AS CHAR) = 'Lead' + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: note + - table: entity_email_address + mode: skip + reason: read in full, every column, by this contract's own surface + - table: lead + mode: skip + reason: read in full, every column, by this contract's own surface + views: + email_address: raw_email_address + settle_s: 1 + settle_attempts: 5 + new_row_fields: + email_address: + - deleted + - id + - invalid + - lower + - opt_out + ignore_columns: + account: + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/report.json b/packages/flow/benchmark/generate_contracts/results/espocrm/report.json index a591f68f12..173dc0a68b 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/report.json +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/report.json @@ -25,13 +25,14 @@ "locations": { "account": "contact.account_id -> account.name (exact)", "account_city": "contact.account_id -> account.billing_address_city (exact)", - "email": "entity_email_address (whose entity_id is the contact key).email_address_id -> email_address.name (exact)", + "email": "entity_email_address (whose entity_id is the contact key).email_address_id -> email_address.name (email)", "first_name": "contact.first_name (exact)", "last_name": "contact.last_name (exact)", "phone": "entity_phone_number (whose entity_id is the contact key).phone_number_id -> phone_number.name (phone)", "title": "account_contact (whose contact_id is the contact key).role (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": { @@ -72,6 +73,7 @@ "stage": "opportunity.stage (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, @@ -102,6 +104,16 @@ "last_name": "lead.last_name (exact)" }, "placed_by_name": [], + "versions": { + "kind": "audit", + "table": "action_history_record", + "record_column": "target_id", + "author_column": "user_id", + "actor_param": "bot_user_id", + "where": { + "target_type": "Lead" + } + }, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, @@ -139,6 +151,7 @@ "subject": "call.name (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": { diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 904491f9f7..f42d88322e 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -915,7 +915,13 @@ def _version_column(schema: Schema, plan: KindPlan) -> Optional[dict[str, Any]]: if len(t.primary_key) != 1: return None for col in t.columns: - if col.kind != "number" or col.name in t.primary_key: + # Named as a version (``lock_version``, ``version_number``): a user + # id that happens to go from 1 to 2 is not one. + if ( + col.kind != "number" + or col.name in t.primary_key + or "version" not in col.name.lower() + ): continue ok = bool(plan.anchors) for a in plan.anchors: @@ -968,9 +974,18 @@ def _audit_source( here.add((lc, ac, p)) rows.append(r) pairs = here if pairs is None else pairs & here - if not pairs or len(pairs) != 1: + if not pairs or len({p[0] for p in pairs}) != 1: continue - lc, ac, param = next(iter(pairs)) + # Several columns hold the acting user (author and assignee): the + # one named as the author, else as a user, else the first by name. + lc, ac, param = sorted( + pairs, + key=lambda p: ( + "author" not in p[1].lower(), + "user" not in p[1].lower(), + p[1], + ), + )[0] # Only a column naming the record's type (``target_type: Lead``) # narrows the read. Anything else the demonstrations had in common # (the action, the client address) could hide another user's save, From ab43f2af550be04a632f4d26001d195559709e58 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 18:18:59 -0400 Subject: [PATCH 35/94] Lint, types, public artifact inventory, capability status --- CAPABILITIES.md | 2 +- llms-full.txt | 2 +- .../openadapt_flow/generate/demo_analysis.py | 8 +-- .../flow/openadapt_flow/generate/sql_store.py | 10 +-- .../generate/verify_contract.py | 64 ++++++++++--------- packages/flow/public-artifacts.json | 31 +++++++++ .../tests/test_generate_verify_contract.py | 5 +- 7 files changed, 80 insertions(+), 42 deletions(-) diff --git a/CAPABILITIES.md b/CAPABILITIES.md index 047b38de1d..59fe9307d6 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,7 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | On main ([#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672)) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/llms-full.txt b/llms-full.txt index b7966432e4..e0dcb8f5ed 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -544,7 +544,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | On main ([#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672)) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index eb576f8158..cb3c71cd51 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -120,15 +120,15 @@ def rule_for(stored: Optional[str], column_kind: str, value: Any) -> Optional[st if column_kind == "text": return None if column_kind in ("number", "bool"): - a, b = _decimal(stored), _decimal(text) - if a is not None and b is not None and a == b: + da, db = _decimal(stored), _decimal(text) + if da is not None and db is not None and da == db: return "number" return None if column_kind == "date": return "date" if stored[:10] == text and len(text) == 10 else None if column_kind == "datetime": - a, b = _datetime(stored.split(".")[0]), _datetime(text) - if a is not None and b is not None and a == b: + ta, tb = _datetime(stored.split(".")[0]), _datetime(text) + if ta is not None and tb is not None and ta == tb: return "datetime" return None return None diff --git a/packages/flow/openadapt_flow/generate/sql_store.py b/packages/flow/openadapt_flow/generate/sql_store.py index 7feeede142..db55cab8b8 100644 --- a/packages/flow/openadapt_flow/generate/sql_store.py +++ b/packages/flow/openadapt_flow/generate/sql_store.py @@ -265,9 +265,10 @@ def _mysql_schema(cur: Any) -> Schema: ) ) for tname, cname, cons, rtable, rcol in keys: - t = tables.get(tname) - if t is None: + kt = tables.get(tname) + if kt is None: continue + t = kt if cons == "PRIMARY": t.primary_key.append(str(cname)) elif rtable: @@ -327,9 +328,10 @@ def _pg_schema(cur: Any) -> Schema: (schema,), ) for tname, ctype, cname, rtable, rcol in cons: - t = tables.get(str(tname).split(".")[-1].strip('"')) - if t is None: + ct = tables.get(str(tname).split(".")[-1].strip('"')) + if ct is None: continue + t = ct if ctype == "p": t.primary_key.append(str(cname)) elif rtable and rcol: diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index f42d88322e..f759afda21 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -566,7 +566,7 @@ def digits(v: str) -> str: a, b = other.columns[0], loc.columns[0] for fn in (str.lower, digits): if all( - r.get(b) == (fn(r[a]) if r.get(a) is not None else None) + r.get(b) == (fn(str(r[a])) if r.get(a) is not None else None) for r in rows.rows.values() ) and any(r.get(a) != r.get(b) for r in rows.rows.values()): return True @@ -649,9 +649,9 @@ def _children( todo.append((p, p, loc, None)) for i, x in enumerate(plan.extra.get(p, [])): todo.append((f"{p}#{i + 2}", p, x, None)) - for dv, locs in plan.dispatch_locations.items(): + for value, locs in plan.dispatch_locations.items(): for p, loc in locs.items(): - todo.append((f"{p}@{dv}", p, loc, dv)) + todo.append((f"{p}@{value}", p, loc, value)) for gname, p, loc, dv in todo: if not loc.hops or loc.hops[0].kind != "child": continue @@ -664,7 +664,7 @@ def _children( for d, a in zip(plan.demos, plan.anchors): if d.values.get(p) is None: continue - if dv is not None and str(d.values.get(plan.dispatch)) != dv: + if dv is not None and str(d.values.get(plan.dispatch or "")) != dv: continue for ch in d.diff.of(loc.hops[0].table, "added"): if ch.after is None: @@ -745,17 +745,16 @@ def _children( plan.refused.append(f"{p}: list items must be mappings") return for i, ch in enumerate(children): - if i in used: + if i in used or ch.after is None: continue - locs = { - k: locate( - schema, d, _as_change(ch.table, ch.after), v, max_hops=1 - ) + after = ch.after + found_at = { + k: locate(schema, d, _as_change(ch.table, after), v, max_hops=1) for k, v in item.items() } - if all(locs.values()): + if all(found_at.values()): used.add(i) - link_col = next(c for c, v in ch.after.items() if v == key) + link_col = next(c for c, v in after.items() if v == key) table = ch.table link = Hop( "child", @@ -763,28 +762,28 @@ def _children( link_col, schema.tables[plan.anchor].primary_key[0], ) - for k, ls in locs.items(): + for k, ls in found_at.items(): per_key.setdefault(k, []).append(ls) - rows.append((d, ch.after)) + rows.append((d, after)) break else: plan.unresolved[f"{p} item {item}"] = [] if table is None or link is None: plan.unresolved.setdefault(p, []) continue - locs = {} + item_locs: dict[str, Location] = {} for k, per in per_key.items(): - loc, tied = best(_intersect(per)) - if loc is None: + item_loc, tied = best(_intersect(per)) + if item_loc is None: plan.unresolved[f"{p}.{k}"] = tied return - locs[k] = loc - explained = {link.column} | {c for loc in locs.values() for c in loc.columns} - explained |= {loc.hops[0].column for loc in locs.values() if loc.hops} + item_locs[k] = item_loc + explained = {link.column} | {c for x in item_locs.values() for c in x.columns} + explained |= {x.hops[0].column for x in item_locs.values() if x.hops} plan.lists[p] = ChildGroup( table, link, - locs, + item_locs, column_roles(schema, table, rows, explained, {}), list_param=p, ) @@ -817,9 +816,9 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N added_keys: set[str] = set() for ch in d.diff.changes: if ch.kind == "added" and ch.after is not None: - t = schema.tables.get(ch.table) - if t is not None and len(t.primary_key) == 1: - v = ch.after.get(t.primary_key[0]) + at = schema.tables.get(ch.table) + if at is not None and len(at.primary_key) == 1: + v = ch.after.get(at.primary_key[0]) if v is not None: added_keys.add(v) per_table: dict[str, list[dict[str, Optional[str]]]] = {} @@ -869,15 +868,15 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N continue per_table.setdefault(ch.table, []).append(ch.after or {}) i = plan.demos.index(d) - for t, rows in per_table.items(): + for t, added in per_table.items(): counts.setdefault(t, [0] * len(plan.demos)) - counts[t][i] = max(len(rows), 1 if t in child_tables else 0) - pure.setdefault(t, []).append([(d, r) for r in rows]) + counts[t][i] = max(len(added), 1 if t in child_tables else 0) + pure.setdefault(t, []).append([(d, r) for r in added]) d.__dict__["_added_keys"] = added_keys for t, cs in sorted(counts.items()): rows = [r for group in pure.get(t, []) for r in group] if len(set(cs)) == 1 and cs[0] > 0 and not looks_like_log(t) and rows: - links = [getattr(d, "_added_keys", set()) for d, _ in rows] + links: list[set[str]] = [getattr(d, "_added_keys", set()) for d, _ in rows] roles = column_roles(schema, t, rows, set(), actor_values, links) plan.sides.append(SideSpec(t, cs[0], roles, unless=child_tables.get(t))) elif rows: @@ -1493,7 +1492,7 @@ def build_contract( name = f"rows:{g.table}" sname = _alias(name) if sname not in surfaces: - cols = _base_columns(b, g.table) + scols = _base_columns(b, g.table) link = g.link # The main record's values, seen from the child row. for ip in identity_params: @@ -1506,8 +1505,10 @@ def build_contract( expr = b.path("t", g.table, up) if loc.rule in ("number", "date", "datetime", "minutes"): expr = b.flag(expr, loc.rule, b.ph(ip)) - cols.append((f"main__{_alias(ip)}", expr)) - surfaces[sname] = Surface(sname, settings.views.get(g.table, g.table), cols) + scols.append((f"main__{_alias(ip)}", expr)) + surfaces[sname] = Surface( + sname, settings.views.get(g.table, g.table), scols + ) named_tables.add(g.table) surf = surfaces[sname] items = range(variant.lists[p]) if g.list_param else [None] @@ -2105,7 +2106,8 @@ def dry_read(path: Path, params: Mapping[str, Any], mysql: bool) -> Optional[str ) from openadapt_flow.verify_run import _audit_sql, _db_connect, _ref_columns - database = contract.config.frame.database + frame = contract.config.frame + database: Any = frame.database if frame is not None else None try: take_snapshot( _db_connect(database.read), diff --git a/packages/flow/public-artifacts.json b/packages/flow/public-artifacts.json index 023180f5a1..78f8e1a024 100644 --- a/packages/flow/public-artifacts.json +++ b/packages/flow/public-artifacts.json @@ -272,6 +272,37 @@ "benchmark/frappe_lending/environment.lock.json", "benchmark/gate_per_class/results/after.json", "benchmark/gate_per_class/results/before.json", + "benchmark/generate_contracts/results/dolibarr/answers.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml", + "benchmark/generate_contracts/results/dolibarr/manifest.yaml", + "benchmark/generate_contracts/results/dolibarr/report.json", + "benchmark/generate_contracts/results/espocrm/answers.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml", + "benchmark/generate_contracts/results/espocrm/manifest.yaml", + "benchmark/generate_contracts/results/espocrm/report.json", + "benchmark/generate_contracts/results/redmine/answers.yaml", + "benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml", + "benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml", + "benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml", + "benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml", + "benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml", + "benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml", + "benchmark/generate_contracts/results/redmine/manifest.yaml", + "benchmark/generate_contracts/results/redmine/report.json", "benchmark/governed_run/results.json", "benchmark/grounding_eval/results.json", "benchmark/grounding_eval/results_sweep.json", diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index 1d5b5aec92..62247e50d9 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -382,7 +382,10 @@ def test_an_update_already_in_place_with_no_write_is_confirmed( app = generated["app"] v = {"project": "Help Desk", "subject": "VPN drops", "status": None, "assignee": None, "due_date": None, "done_ratio": 80, "notes": None} # fmt: skip - app.q("UPDATE issues SET done_ratio=80 WHERE id=%s", (app.find("Help Desk", "VPN drops"),)) + app.q( + "UPDATE issues SET done_ratio=80 WHERE id=%s", + (app.find("Help Desk", "VPN drops"),), + ) assert _run(generated, "update_issue", v, lambda: None).startswith("CONFIRMED") # The same task with a stray write on the issue is stopped. stray = _run(generated, "update_issue", v, From a1d9674ebbaaddf12d2ca2620acddca494067668 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 18:45:20 -0400 Subject: [PATCH 36/94] Final real-app results on the rebased branch --- .../benchmark/generate_contracts/RESULTS.md | 26 +++++++++++++------ ...w_email+new_phone+town.110f7554.found.yaml | 14 ++++++++++ ..._email+new_phone+town.110f7554.update.yaml | 14 ++++++++++ .../results/dolibarr/report.json | 11 ++++++++ ...project+status+subject.4aa4d777.found.yaml | 14 ++++++++++ ...roject+status+subject.4aa4d777.update.yaml | 14 ++++++++++ .../results/redmine/report.json | 11 ++++++++ 7 files changed, 96 insertions(+), 8 deletions(-) diff --git a/packages/flow/benchmark/generate_contracts/RESULTS.md b/packages/flow/benchmark/generate_contracts/RESULTS.md index 4c0c0e5817..b686b7dda2 100644 --- a/packages/flow/benchmark/generate_contracts/RESULTS.md +++ b/packages/flow/benchmark/generate_contracts/RESULTS.md @@ -25,11 +25,12 @@ Late-write faults also ran the recheck the contract declares. | Questions for a person | **3** | **1** | **2** | | First study, person decisions | 11 | 11 | 11 | | Contracts written | 93 | 95 | 235 | -| Correct writes confirmed (tasks and controls) | 40/43 | 35/38 | 35/40 | +| Correct writes confirmed (tasks and controls) | 40/43 | 35/38 | 36/40 | | In-scope planted faults stopped | **22/22** | **19/19** | **21/21** | | First study, in-scope faults stopped | 21/21 | 26/26 | 16/19 | | Out-of-scope late writes stopped | 2/2 (recheck) | 0/2 (no recheck on PostgreSQL) | 1/2 (recheck) | -| `propose` time | 1.5 min | 2.5 min | 2 min | +| Lost-update versions (`frame.versions`) | agenda events (`llx_actioncomm`) | journals | `action_history_record` | +| `propose` time | 1.5 min | 1.5 min | 2 min | The first study measured false stops on agents' writes; here the correct writes are the pack's reference writes and the controls, so the two false @@ -53,7 +54,10 @@ Every answer was the proposal, except the one choice in Dolibarr's question 3. - Dolibarr and EspoCRM: `GRANT TRIGGER ON .* TO ` (`--grant-trigger`): the whole-database frame must see triggers. -- EspoCRM before the SQL guard fix (#1665): a view `espocrm_oa_read.oa_read_call` (`--create-views`), because the guard refused the table name `call`. After the rebase the guard reads `call` and no view is needed. +- Before the SQL guard fix (#1665), EspoCRM also needed a view + `espocrm_oa_read.oa_read_call` (`--create-views`), because the guard + refused the table name `call`. On `main` the guard reads `call`, and the + final run created no view. ### False stops, by cause @@ -68,12 +72,15 @@ Every answer was the proposal, except the one choice in Dolibarr's question 3. | EspoCRM | T21 | The reference write removes the old email address row when replacing it; the demonstration didn't | | EspoCRM | T22 | `field: phone` has no contract: no demonstration, and phones live in another table | | EspoCRM | F22, F27 (controls) | Another user's unrelated edit or new lead: not attributed | -| EspoCRM | F24 (control) | Mixed-case email and padded name (emails now compare case-folded) | + +The lost-update faults are stopped with a version reason now (EspoCRM F19: +"another user saved a version of the target inside the window"); the first +study missed EspoCRM F19 and HAPI F14. ### Misses Out of scope only: Redmine F19 and F20 (late writes; PostgreSQL has no -recheck). EspoCRM F21 (a late edit to a look-alike lead: an update's main table is read by its own surface, which the recheck doesn't read). No in-scope planted fault was confirmed on any app. +recheck), and EspoCRM F21 (a late edit to a look-alike lead: an update's main table is read by its own surface, which the recheck doesn't read). No in-scope planted fault was confirmed on any app. ## Reproduce @@ -93,6 +100,9 @@ python summarize.py dolibarr WORK full set is regenerated by the commands above). The fault scripts and the oracle stay in the study's private pack. -Code: these runs used this branch before the rebase onto `main`, with #1661 -(the whole-database frame fix) merged in, and before `frame.versions` was -emitted. Reruns on the rebased branch are in progress. +Code: the final runs used this branch rebased on `main` (with #1661 to +#1669): EspoCRM at `baf86e78a`, Dolibarr and Redmine at `c51437ba6` (the +commits after are lint, type and documentation fixes). Earlier runs on the +branch before the rebase, with #1661 merged in, gave the same counts for +Dolibarr and Redmine; for EspoCRM they missed nothing more and confirmed +one write fewer (F24, before emails compared case-folded). diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml index 6b5b5cbc46..9dee225bd5 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml @@ -84,6 +84,20 @@ frame: - socialnetworks - tms target_fields: [] + versions: + read: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`fk_soc` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`fk_user_author` + AS CHAR) AS `author` FROM `llx_actioncomm` t + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id database: dialect: mysql read: diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml index ac485d5716..8355de323a 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml @@ -165,6 +165,20 @@ frame: target_fields: - email - phone + versions: + read: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`fk_soc` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`fk_user_author` + AS CHAR) AS `author` FROM `llx_actioncomm` t + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id database: dialect: mysql read: diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json index 916e1bc81e..4ad05e82bf 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json @@ -39,6 +39,7 @@ "zip": "llx_societe.zip (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, @@ -72,6 +73,7 @@ "vat_rate": "llx_product.tva_tx (number)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": { @@ -107,6 +109,7 @@ "town": "llx_facture.fk_soc -> llx_societe.town (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, @@ -136,6 +139,14 @@ "new_address", "new_zip" ], + "versions": { + "kind": "audit", + "table": "llx_actioncomm", + "record_column": "fk_soc", + "author_column": "fk_user_author", + "actor_param": "bot_user_id", + "where": {} + }, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml index fc5336b4d8..95a96cf13c 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml @@ -574,3 +574,17 @@ frame: - others - updated_on target_fields: [] + versions: + read: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."journalized_id" AS TEXT) AS "id", CAST(t."id" AS TEXT) AS "ver", CAST(t."user_id" + AS TEXT) AS "author" FROM "journals" t + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml index 3c41dbe83f..295dcbaf95 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml @@ -588,3 +588,17 @@ frame: target_fields: - p__status - status_id + versions: + read: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."journalized_id" AS TEXT) AS "id", CAST(t."id" AS TEXT) AS "ver", CAST(t."user_id" + AS TEXT) AS "author" FROM "journals" t + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/report.json b/packages/flow/benchmark/generate_contracts/results/redmine/report.json index 188f7efd34..fc31d7963c 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/report.json +++ b/packages/flow/benchmark/generate_contracts/results/redmine/report.json @@ -29,6 +29,7 @@ "placed_by_name": [ "description" ], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": { @@ -60,6 +61,14 @@ "placed_by_name": [ "done_ratio" ], + "versions": { + "kind": "audit", + "table": "journals", + "record_column": "journalized_id", + "author_column": "user_id", + "actor_param": "bot_user_id", + "where": {} + }, "placed_from_another_kind": [ "due_date", "priority" @@ -91,6 +100,7 @@ "spent_on": "time_entries.spent_on (date)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, @@ -114,6 +124,7 @@ "tracker": "issues.tracker_id -> trackers.name (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, From f48868d74409abb0b5042ec6b7ed3e0dce8fc90a Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 19:12:18 -0400 Subject: [PATCH 37/94] Round 1 review: confirmed phone rule, row-scoped bookkeeping, found pins constants, unique live display values, identity-only child links, no name-based logs, write-event version sources, quoted grants; docs --- CAPABILITIES.md | 2 +- llms-full.txt | 2 +- packages/flow/docs/GENERATE_CONTRACTS.md | 111 ++-- .../openadapt_flow/generate/demo_analysis.py | 38 +- .../generate/verify_contract.py | 590 +++++++++++++++--- .../tests/test_generate_verify_contract.py | 5 +- .../test_generate_verify_contract_attacks.py | 263 ++++++++ 7 files changed, 856 insertions(+), 155 deletions(-) create mode 100644 packages/flow/tests/test_generate_verify_contract_attacks.py diff --git a/CAPABILITIES.md b/CAPABILITIES.md index 59fe9307d6..9abfd9b170 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,7 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | On main ([#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672)) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/llms-full.txt b/llms-full.txt index e0dcb8f5ed..8d87bf3b62 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -544,7 +544,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | On main ([#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672)) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 6013304c66..db9d014833 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -119,12 +119,19 @@ that as a stop: the write must not be sent unchecked. | Rule | Example | | --- | --- | | The stored text equals the value | `subject` | -| Equal after case folding, for any value shaped like an email address | `Ana@Example.org` | -| Equal digits, for any value shaped like a phone number | `02 38 53 21 09` stored as `0238532109` | +| Equal after case folding, for a value shaped like an email address | `Ana@Example.org` | +| Equal digits, only for a parameter a person confirmed as a phone number (the `phones` question), when the app stored other text with the same digits | `02 38 53 21 09` stored as `0238532109` | | Equal as a number, a date or a date-time, compared in SQL | `9.20` stored as `9.2`; `2026-10-27 14:30` stored as `2026-10-27 14:30:00` | | Minutes between two date-time columns of the row | a call's duration | | A display column of the row a key column points to | `project_id` -> `projects.name` | | Two text columns joined by one space | `firstname` and `lastname` | + +A display value behind a key must name exactly one row of its table that +isn't soft-deleted (a `deleted` or `is_deleted` flag, or a `deleted_at` +time): a second "Jon Park", or "Mary Ann Lee" split as `Mary Ann|Lee` and +`Mary|Ann Lee`, makes the value empty, so the write stops. At run time a +confirmed phone value that isn't written like a phone number (fewer than 7 +digits, or other characters) is compared exactly. | A row the write added that holds the main record's key | a journal note, a custom field value | | A row it added that links the record to the row holding the value | an email address, a call's attendee | @@ -169,51 +176,69 @@ too. A list parameter gets a contract per length, from 1 to 10 items. - `create`: exactly one new main record holds the task's values, and everything else in the new row is what the demonstrations showed, or is bookkeeping. -- `found`: the record already holds the task's values, and nothing in the - database changed. `choose` reads the store first and picks `found` when a - record with the task's values is already there: a "make sure it exists" - task whose record exists, or an update that is already in place. +- `found`: the record already holds the task's values and, for a create, + the same constants the demonstrations' new rows held (a soft-delete flag, a + status, a type), and nothing in the database changed. `choose` reads the + store first and picks `found` when such a record is already there: a "make + sure it exists" task whose record exists, or an update that is already in + place. A trashed record with the task's values is not found. - `update`: exactly one record matches the task's identity values and holds - the new values. Only the fields the task changes may change. When the + the new values. Only the fields the task changes may change, plus the + target's own bookkeeping columns a person confirmed. When the demonstrations replaced the record's rows beside it (an old email address link removed), the earlier rows may go, and afterwards the record has as many such rows as the demonstrations left. -**Columns of a new row no value explains.** A key, a time stamp from the -write, the acting user, a link to a row the write added, the year, month, -week or day of a date the task gives, a generated token (a UUID), or a copy -of a column of the row a key points to (an invoice line's price from its -product) is recognized as such. Free text the demonstrations left empty is -not checked. Any other column must hold the value the demonstrations -showed; one that differed between them is not checked and is listed in the -bookkeeping question. +**Columns of a new row no value explains.** Each is pinned to the acting +user, to the value every demonstration held, or to a copy of a column of the +row a key points to (an invoice line's price from its product). A key, a +link to a row the write added, the year, month, week or day of a date the +task gives, and a generated token (a UUID) are recognized as such. Columns +that differ between demonstrations, free text every demonstration left +empty, and the write's time stamps are not checked; the bookkeeping question +lists each of them by table, so a person sees every column left open. **Rows the app adds on its own.** A row type the app added the same number of times in every demonstration (an agenda event, a stream note, a price-history row) is declared with that count and its constant columns. A table whose -added rows vary between demonstrations, or whose name says log, history, -audit or similar, is proposed as a log of that kind. Columns that changed -with no task value (a modified time, a version counter) are proposed as -bookkeeping. All of it goes in one question. +added rows differ in number between demonstrations is listed in the +bookkeeping question and is a log only if the person names it (`logs_add`); +otherwise every added row stops the write. The generator never decides a +table is a log from its name. + +**Changes the app makes to existing rows.** A column of an existing row that +the demonstrations changed with no task value is excused only on that row: +the target itself, a row with the same key in every demonstration (a counter +row), or the row the task's values name through a key of the main record +(the account a contact belongs to). It is never excused by name on other +rows or tables. Time stamps set during the write, counters that went up and +columns set to the acting user are proposed; anything else is listed and +excused only if the person adds it (`columns_add`). A changed row the +generator can't identify is listed too, and a write that changes it stops. **The frame.** On MySQL and MariaDB every contract uses the whole-database frame (`frame.database`): a row added, removed or changed in any table that -the contract doesn't declare stops the write. The main record's table and -the tables of rows that hold values are read in full by the contract's own -reads, with every column. On PostgreSQL, `frame.database` isn't available, so -every other table is read as its own surface; tables proposed as logs are -not read there. +the contract doesn't declare stops the write, except rows added to a table +the person named as a log. The main record's table and the tables of rows +that hold values are read by the contract's own reads, with every column. +On PostgreSQL, `frame.database` isn't available, so every other table is read +as its own surface, except the tables the person named as logs, which are +not read at all. **Lost updates.** An update's contracts set `frame.versions`, so another user's save to the target inside the window stops the write. The generator takes the versions from the app's own audit trail when every update demonstration added a row there that names the record and the acting user (Redmine's journals, EspoCRM's `action_history_record`, Dolibarr's agenda -events); the read is narrowed only by a column naming the record's type, -so any other user's entry for the record counts. Otherwise it uses a whole -number column of the record that went up by one in every demonstration -(Redmine's `lock_version`). With neither, the update has no version check, -and `report.json` shows `versions: null`. +events). The read is narrowed to the record's type and to write events: a +column whose value in the update rows differs from its value in the rows +creates added to the same table (EspoCRM's `action`, Dolibarr's `code`), so +another user only viewing the record doesn't count. Otherwise it uses a +whole-number column named as a version that went up by one in every +demonstration (Redmine's `lock_version`). With neither, the update has no +version check, and `report.json` shows `versions: null`. If the app stops +writing the trail (an agenda setting turned off), the agent's own save has +no new version, and the verifier stops the write rather than pass it. **The recheck window.** On MySQL and MariaDB, one question asks how long the app can still write on its own after a verified write. The answer is @@ -226,10 +251,12 @@ that needs something the store doesn't have is either set up, with a flag and an admin connection you give, or not written, with the reason. - `--grant-trigger --admin-read admin.yaml --reader-account "'oa_reader'@'%'"`: - grants `TRIGGER` on the database to the read account (MySQL, MariaDB). + grants `TRIGGER` on the database to the read account (MySQL, MariaDB). The + account name is split into user and host and escaped by the driver. Without it, the generator stops and prints the `GRANT` statement for the - DBA. Note that `TRIGGER` also lets the account create triggers. The verifier - needs it only to see them. + DBA. `TRIGGER` also lets the account create and drop triggers; the verifier + needs it only to see them, and `report.json` says so under `read_account`. + Keep that account's password away from the agent. - `--create-views --admin-read admin.yaml --reader-account ...`: the verifier's SQL guard refuses some table names in a read (EspoCRM's `call`). This creates a view `oa_read_
` in a schema of its own, @@ -252,18 +279,18 @@ why. - A list item that appears twice in one task (the same product and quantity twice) is stopped: each item must match one new row. - Columns of a new record that differ between demonstrations and aren't - task values (an invoice's computed totals, its draft reference) are not - checked. The bookkeeping question lists them. -- Free-text columns of a new record that the task doesn't give, and that - every demonstration left empty (a line description, a note), are not - checked: an agent may fill them from the record it copies. + task values (an invoice's computed totals, its due date), free text every + demonstration left empty, and time stamps are not checked. The bookkeeping + question lists every one of them. - With one demonstration of a create, every other column must hold the value it held then, including one computed from the task's values (a price with tax). `QUESTIONS.md` says so; record a second create with other values. -- On PostgreSQL there is no whole-database frame and no recheck: tables - proposed as logs are not read, and a write that lands after the check is - not seen. +- On PostgreSQL there is no whole-database frame, so there is no recheck (a + write that lands after the check is not seen), and a table the person names + as a log is not read at all: a rewrite of its existing rows passes. Every + other table is read and compared. - REST metadata (OpenAPI) is not read yet. Apps without database access need hand-written contracts. -- The generator does not read an audit trail, so another user's edit during - the window stops the write instead of being attributed to them. +- The frame doesn't attribute changes to other users: another user's edit to + an unrelated record during the window stops the write. Only the target's + own versions are read from the audit trail (lost updates). diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index cb3c71cd51..6b37d78234 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -49,12 +49,19 @@ _IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") _EMAIL = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") _PHONE = re.compile(r"^\+?[0-9][0-9 ().\-/]*$") -#: Table names an app's own logs usually carry. Only a proposal: a person -#: confirms which tables are logs (the bookkeeping question). -_LOG_NAME = re.compile( - r"(log|history|audit|session|token|notification|queue|job|stat|cache|lock|event)", - re.I, -) +#: Fewest digits a value compared as a phone number may have. +PHONE_MIN_DIGITS = 7 + + +def phone_shaped(value: Any) -> bool: + """Is ``value`` written like a phone number (digits and ``+ ( ) - . /`` + and spaces, with at least :data:`PHONE_MIN_DIGITS` digits)?""" + + text = value_text(value) if value is not None else "" + return ( + bool(_PHONE.match(text)) + and sum(ch.isdigit() for ch in text) >= PHONE_MIN_DIGITS + ) def value_text(value: Any) -> str: @@ -102,13 +109,18 @@ def rule_for(stored: Optional[str], column_kind: str, value: Any) -> Optional[st if text == "": return None if column_kind == "text": - # An email address or a phone number is compared in its declared - # canonical form even when this demonstration's text matches - # exactly: another write may store ``Ana@Example.org`` or - # ``+16175550144`` for the same value. + # An email address is compared case-folded even when this + # demonstration's text matches exactly (another write may store + # ``Ana@Example.org``). A phone number only when the app stored other + # text with the same digits, and only as a proposal: a person + # confirms it (the ``phones`` question), never the value's shape. if _EMAIL.match(text) and stored.casefold() == text.casefold(): return "email" - if _PHONE.match(text) and sum(ch.isdigit() for ch in text) >= 6: + if ( + stored != text + and _PHONE.match(text) + and sum(ch.isdigit() for ch in text) >= PHONE_MIN_DIGITS + ): a, b = kind_form("phone", stored), kind_form("phone", text) if a is not None and a == b: return "phone" @@ -516,10 +528,6 @@ def snake(name: str) -> str: return re.sub(r"(? bool: - return bool(_LOG_NAME.search(table)) - - @dataclass class ColumnRole: """What a column of a new row is, when no parameter explains it.""" diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index f759afda21..70e9636752 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -58,7 +58,6 @@ is_identifier, link_columns, locate, - looks_like_log, pick_anchor, ) from openadapt_flow.generate.sql_store import RowChange, Schema @@ -249,6 +248,18 @@ class KindPlan: by_name: list[str] = field(default_factory=list) #: Parameters placed where another kind's demonstrations found them. shared: list[str] = field(default_factory=list) + #: Existing rows (not the target) the demonstrations changed with no + #: task value: each the same row every time (by key) or the row a + #: located value names (an account behind the contact's key), with the + #: columns that moved and what kind of change each was. + touched: list[dict[str, Any]] = field(default_factory=list) + #: The target's own columns that moved with no task value, by kind. + anchor_extra: dict[str, str] = field(default_factory=dict) + #: Changed rows nothing identifies: a write that changes them stops. + unexcused: dict[str, set[str]] = field(default_factory=dict) + #: Tables whose added rows differ between demonstrations: logs only if a + #: person says so. + log_candidates: set[str] = field(default_factory=set) #: Where the target's versions are read (``frame.versions``): a version #: column that steps by one per save, or an audit table naming the #: record and the user who saved it. None when the demonstrations show @@ -451,7 +462,7 @@ def analyze( "target can't be told apart from other records" ) _children(schema, plan, lists) - _sides(schema, plan, actor_values) + _sides(schema, plan, actor_values, demos) return plan @@ -573,6 +584,32 @@ def digits(v: str) -> str: return False +def _all_locations(plan: KindPlan) -> list[tuple[str, Location]]: + out = list(plan.locations.items()) + for locs in plan.dispatch_locations.values(): + out.extend(locs.items()) + for g in [*plan.children.values(), *plan.lists.values()]: + out.extend(g.params.items()) + return out + + +def _confirm_phones(plan: KindPlan, confirmed: set[str]) -> None: + """A phone rule a person didn't confirm compares the text exactly.""" + + def fix(name: str, loc: Location) -> Location: + if loc.rule == "phone" and name not in confirmed: + return Location(loc.hops, loc.columns, "exact") + return loc + + plan.locations = {k: fix(k, v) for k, v in plan.locations.items()} + plan.dispatch_locations = { + dv: {k: fix(k, v) for k, v in locs.items()} + for dv, locs in plan.dispatch_locations.items() + } + for g in [*plan.children.values(), *plan.lists.values()]: + g.params = {k: fix(k, v) for k, v in g.params.items()} + + def _resolve(plan: KindPlan, key: str, loc: Location) -> None: """Place an unresolved value (answered, or settled by a tie-break).""" @@ -805,13 +842,19 @@ def _demo_groups(plan: KindPlan, d: Demo) -> list[ChildGroup]: return _all_groups(plan, dv) if dv is not None else list(plan.children.values()) -def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> None: +def _sides( + schema: Schema, + plan: KindPlan, + actor_values: Mapping[str, str], + all_demos: Sequence[Demo] = (), +) -> None: """Classify every other row the demonstrations touched.""" child_tables = {g.table: g.param for g in _all_groups(plan)} list_tables = {g.table for g in plan.lists.values()} pure: dict[str, list[list[tuple[Demo, dict[str, Optional[str]]]]]] = {} counts: dict[str, list[int]] = {} + touched: list[tuple[Demo, RowChange, RowChange]] = [] for d, a in zip(plan.demos, plan.anchors): added_keys: set[str] = set() for ch in d.diff.changes: @@ -846,14 +889,8 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N g.changed_columns = sorted( set(g.changed_columns) | set(ch.changed_columns) ) - elif ch.table == plan.anchor: - plan.bookkeeping.setdefault(ch.table, set()).update( - ch.changed_columns - ) - else: - plan.bookkeeping.setdefault(ch.table, set()).update( - ch.changed_columns - ) + elif ch.table != plan.anchor or ch.key != a.key: + touched.append((d, a, ch)) continue # added if ch.table in list_tables: @@ -875,12 +912,16 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N d.__dict__["_added_keys"] = added_keys for t, cs in sorted(counts.items()): rows = [r for group in pure.get(t, []) for r in group] - if len(set(cs)) == 1 and cs[0] > 0 and not looks_like_log(t) and rows: + if len(set(cs)) == 1 and cs[0] > 0 and rows: links: list[set[str]] = [getattr(d, "_added_keys", set()) for d, _ in rows] roles = column_roles(schema, t, rows, set(), actor_values, links) plan.sides.append(SideSpec(t, cs[0], roles, unless=child_tables.get(t))) elif rows: - plan.logs.add(t) + # The number of added rows differs between demonstrations: a log + # only when a person says so (bookkeeping question). Until then + # every added row stops the write. + plan.log_candidates.add(t) + _touched_rows(schema, plan, touched, actor_values) if plan.mode == "update": for a in plan.anchors: plan.anchor_changed |= set(a.changed_columns) @@ -896,15 +937,145 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N for loc in locs.values(): explained |= set(loc.columns) if not loc.hops else {loc.hops[0].column} extra = plan.anchor_changed - explained - if extra: - plan.bookkeeping.setdefault(plan.anchor, set()).update(extra) - plan.versions = _audit_source(schema, plan, actor_values) or _version_column( - schema, plan - ) + for col in sorted(extra): + plan.anchor_extra[col] = _change_kind( + schema, plan.anchor, col, plan.demos, plan.anchors, actor_values + ) + plan.versions = _audit_source( + schema, plan, actor_values, all_demos + ) or _version_column(schema, plan) _WHOLE_NUMBER = re.compile(r"^-?[0-9]+$") +#: Kinds of change a person is offered as the app's own bookkeeping by +#: default. Anything else is listed but not excused unless they add it. +BOOKKEEPING_KINDS = ("timestamp", "counter", "actor") + + +def _change_kind( + schema: Schema, + table: str, + col: str, + demos: Sequence[Demo], + changes: Sequence[RowChange], + actor_values: Mapping[str, str], +) -> str: + """``timestamp`` (set to a time inside the demonstration), ``counter`` + (a whole number that went up), ``actor`` (set to the acting user) or + ``other``, the same in every demonstration that changed it.""" + + from openadapt_flow.generate.demo_analysis import _within + + column = schema.tables[table].column(col) + kinds = set() + for d, ch in zip(demos, changes): + before = (ch.before or {}).get(col) + after = (ch.after or {}).get(col) + if before == after: + continue + if ( + column is not None + and column.kind in ("date", "datetime") + and _within(after, d.started_at, d.finished_at) + ): + kinds.add("timestamp") + elif ( + column is not None + and column.kind in ("number", "text") + and before is not None + and after is not None + and _WHOLE_NUMBER.match(before) + and _WHOLE_NUMBER.match(after) + and int(after) > int(before) + ): + kinds.add("counter") + elif after is not None and after in set(actor_values.values()): + kinds.add("actor") + else: + kinds.add("other") + return kinds.pop() if len(kinds) == 1 else "other" + + +def _touched_rows( + schema: Schema, + plan: KindPlan, + touched: Sequence[tuple[Demo, RowChange, RowChange]], + actor_values: Mapping[str, str], +) -> None: + """Identify each existing row a demonstration changed with no task + value, so an excuse applies to that row only: the same row (by key) in + every demonstration (a counter row), or the row a located value names + through a key of the main record (the contact's account).""" + + by_table: dict[str, list[tuple[Demo, RowChange, RowChange]]] = {} + for item in touched: + by_table.setdefault(item[2].table, []).append(item) + for table, items in sorted(by_table.items()): + t = schema.tables.get(table) + cols = sorted({c for _, _, ch in items for c in ch.changed_columns}) + if t is None or len(t.primary_key) != 1: + plan.unexcused.setdefault(table, set()).update(cols) + continue + pk = t.primary_key[0] + keys = {ch.key for _, _, ch in items} + demos_with = {id(d) for d, _, _ in items} + selector: Optional[dict[str, Any]] = None + how = "" + if ( + len(keys) == 1 + and len(demos_with) == len(plan.demos) + and len(items) == len(plan.demos) + ): + key = (items[0][2].after or {}).get(pk) + if key is not None: + selector = {pk: {"literal": key}} + how = f"{table} row {pk}={key}" + if selector is None: + # Reached from the main record by a key column whose target row + # holds located values in single columns. + for p, loc in plan.locations.items(): + if ( + not loc.hops + or loc.hops[0].kind != "fk" + or loc.hops[0].table != table + ): + continue + hop = loc.hops[0] + if all( + (a.after or {}).get(hop.column) == (ch.after or {}).get(pk) + for _, a, ch in items + ): + sel = { + x.columns[0]: {"param": q} + for q, x in plan.locations.items() + if len(x.hops) == 1 + and x.hops[0] == hop + and len(x.columns) == 1 + and x.rule == "exact" + } + if sel: + selector = sel + how = f"the {table} row the task's {', '.join(sorted(v['param'] for v in sel.values()))} name" + break + if selector is None or len(items) != len({id(d) for d, _, _ in items}): + plan.unexcused.setdefault(table, set()).update(cols) + continue + kinds = { + c: _change_kind( + schema, + table, + c, + [d for d, _, _ in items], + [ch for _, _, ch in items], + actor_values, + ) + for c in cols + } + plan.touched.append( + {"table": table, "selector": selector, "columns": kinds, "row": how} + ) + def _version_column(schema: Schema, plan: KindPlan) -> Optional[dict[str, Any]]: """A whole-number column of the main record that went up by exactly one @@ -941,7 +1112,10 @@ def _version_column(schema: Schema, plan: KindPlan) -> Optional[dict[str, Any]]: def _audit_source( - schema: Schema, plan: KindPlan, actor_values: Mapping[str, str] + schema: Schema, + plan: KindPlan, + actor_values: Mapping[str, str], + all_demos: Sequence[Demo] = (), ) -> Optional[dict[str, Any]]: """A table that gained, in every update demonstration, a row naming the main record (a column holding its key) and the acting user (a column @@ -999,6 +1173,26 @@ def _audit_source( v = next(iter(values)) if v is not None and v.lower() in names and v.isidentifier(): where[col.name] = v + # Write events only: a column whose value in these rows (every + # update) differs from its value in the rows creates added to the + # same table (EspoCRM's ``action``: update, not create or read; + # Dolibarr's ``code``: COMPANY_MODIFY). + created = [ + ch.after or {} + for d in all_demos + if d not in plan.demos + for ch in d.diff.of(name, "added") + ] + for col in table.columns: + if col.name in (lc, ac) or col.name in table.primary_key: + continue + mine = {r.get(col.name) for r in rows} + theirs = {r.get(col.name) for r in created} + if len(mine) == 1 and created and len(theirs) == 1 and mine != theirs: + v = next(iter(mine)) + if v is not None and (col.kind == "text" or _WHOLE_NUMBER.match(v)): + if "\\" not in v and "'" not in v and "%" not in v: + where[col.name] = v found = { "kind": "audit", "table": name, @@ -1175,6 +1369,9 @@ def __init__(self, schema: Schema) -> None: self.params: dict[str, dict[str, str]] = {} #: Tables read through a view (the SQL guard refuses their name). self.views: dict[str, str] = {} + #: The main record's table: a key back to it names the record by its + #: own key, not by a display value, so no uniqueness check applies. + self.anchor = "" self._n = itertools.count() def q(self, name: str) -> str: @@ -1233,11 +1430,48 @@ def path(self, alias: str, table: str, loc: Location, depth: int = 0) -> str: raise GenerateError("a child hop can't be read as one value") sub = f"a{depth + 1}" inner = self.path(sub, hop.table, rest, depth + 1) + live = self.live(sub, hop.table) + if not rest.hops and hop.table != self.anchor: + # The value must name exactly one live row of its table: a + # second "Jon Park", or "Mary Ann|Lee" beside "Mary|Ann Lee", + # makes it NULL, so nothing matches and the write stops. + u = f"u{depth + 1}" + other = self.display(u, hop.table, rest.columns, rest.rule) + mine = self.display(sub, hop.table, rest.columns, rest.rule) + unique = ( + f"(SELECT COUNT(*) FROM {self.tref(hop.table)} {u} WHERE " + f"{self.same(other, mine, False)}{self.live_and(u, hop.table)}) = 1" + ) + inner = f"CASE WHEN {unique} THEN {inner} END" return ( f"(SELECT {inner} FROM {self.tref(hop.table)} {sub} WHERE " - f"{self.same(f'{sub}.{self.q(hop.ref_column)}', f'{alias}.{self.q(hop.column)}', hop.declared)})" + f"{self.same(f'{sub}.{self.q(hop.ref_column)}', f'{alias}.{self.q(hop.column)}', hop.declared)}" + f"{' AND ' + live if live else ''})" ) + def live(self, alias: str, table: str) -> str: + """The condition that a row isn't soft-deleted (a ``deleted`` or + ``is_deleted`` flag, or a ``deleted_at`` time), or "" when the table + has none: a trashed look-alike never resolves a key.""" + + t = self.schema.tables.get(table) + if t is None: + return "" + for name in ("deleted", "is_deleted"): + col = t.column(name) + if col is not None and col.kind in ("number", "bool"): + ref = f"{alias}.{self.q(name)}" + if self.d == "mysql" or col.kind == "number": + return f"COALESCE({ref}, 0) = 0" + return f"NOT COALESCE({ref}, false)" + if t.column("deleted_at") is not None: + return f"{alias}.{self.q('deleted_at')} IS NULL" + return "" + + def live_and(self, alias: str, table: str) -> str: + cond = self.live(alias, table) + return f" AND {cond}" if cond else "" + def same(self, a: str, b: str, declared: bool = True) -> str: if declared: return f"{a} = {b}" @@ -1400,13 +1634,17 @@ def build_contract( variant: Variant, flavor: str, settings: Settings, - bookkeeping: Mapping[str, set[str]], + excused: Mapping[str, set[str]], logs: set[str], ) -> dict[str, Any]: - """One ``openadapt.verify/v1`` document for one field set.""" + """One ``openadapt.verify/v1`` document for one field set. ``excused``: + table -> columns a person confirmed as the app's own bookkeeping; each + is excused only on the rows it was seen changing (the target, or a row + identified by key or by the task's values), never by name elsewhere.""" b = SqlBuilder(schema) b.views = dict(settings.views) + b.anchor = plan.anchor fields = set(variant.fields) anchor = plan.anchor surfaces: dict[str, Surface] = {} @@ -1436,10 +1674,21 @@ def build_contract( if plan.mode == "update" and p in plan.changes: target_fields.add(key) target_fields.update(loc.columns if not loc.hops else [loc.hops[0].column]) - if plan.mode == "create" and flavor == "create": + if plan.mode == "update" and flavor == "update": + # The target's own bookkeeping, excused on the target row only. + target_fields.update( + c for c in plan.anchor_extra if c in excused.get(anchor, set()) + ) + if plan.mode == "create": + # ``found`` pins the same constants as ``create`` (a soft-delete + # flag, a status, a type): a trashed record with the task's values + # is not "already there". Who created it may differ. roles = _anchor_roles(schema, plan, variant, settings.actor_params) for col, role in sorted(roles.items()): + if flavor == "found" and role.role in ("actor", "copied"): + continue _pin(b, main, anchor, col, role, match, settings.actor_params) + if plan.mode == "create" and flavor == "create": support = _support(plan, variant) for p in plan.optional: if p in fields or p in variant.lists: @@ -1488,6 +1737,14 @@ def build_contract( or (not found and plan.mode == "create" and set(g.co_owners) & fields) ] groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] + # A child row names its record by the record's identity: for an update + # only the values that pick the target, never the ones it changes (the + # target's older rows would otherwise move with the change). + main_params = ( + [ip for ip in identity_params if ip in plan.identity] + if plan.mode == "update" + else identity_params + ) for p, g in groups: name = f"rows:{g.table}" sname = _alias(name) @@ -1495,7 +1752,7 @@ def build_contract( scols = _base_columns(b, g.table) link = g.link # The main record's values, seen from the child row. - for ip in identity_params: + for ip in main_params: loc = locations[ip] up = Location( (Hop("fk", anchor, link.column, link.ref_column, True),) + loc.hops, @@ -1514,7 +1771,7 @@ def build_contract( items = range(variant.lists[p]) if g.list_param else [None] for i in items: m: dict[str, Any] = {} - for ip in identity_params: + for ip in main_params: loc = locations[ip] if loc.rule in ("number", "date", "datetime", "minutes"): m[f"main__{_alias(ip)}"] = {"literal": "1"} @@ -1536,7 +1793,7 @@ def build_contract( else: m[key] = _value_expr(pname, loc.rule) for col, role in sorted(g.roles.items()): - if role.role == "link" or found: + if role.role == "link" or (found and role.role in ("actor", "copied")): continue _pin(b, surf, g.table, col, role, m, settings.actor_params) effect = { @@ -1569,6 +1826,32 @@ def build_contract( ) if plan.mode == "update": target_fields.update(g.changed_columns) + # Existing rows the app changes beside the write (a counter row, the + # account a contact belongs to): declared by key or by the task's values, + # with only the confirmed columns allowed to move, on those rows only. + if not found: + for spec in plan.touched: + cols = [ + c for c in spec["columns"] if c in excused.get(spec["table"], set()) + ] + if not cols: + continue + name = f"raw_{_alias(spec['table'])}" + if name not in surfaces: + surfaces[name] = Surface( + name, + settings.views.get(spec["table"], spec["table"]), + _base_columns(b, spec["table"]), + ) + effects.append( + { + "kind": "record_written", + "probe": f"surface={name}|{spec['row']}, changed by the app", + "match": dict(spec["selector"]), + "expected_count": 1, + } + ) + target_fields.update(cols) # A new row a link row names (a new email address). for ref in plan.added_refs.values(): p = ref["param"] @@ -1619,21 +1902,8 @@ def build_contract( verifier["surfaces"][s.name] = sub # Main and child effects read named surfaces: no other read is judged # against them, and the whole-database frame covers every other table. - ignore = sorted( - {c for cols in bookkeeping.values() for c in cols} - & ( - _all_columns(schema, named_tables) - if schema.dialect == "mysql" - else _all_columns(schema, set(schema.tables)) - ) - ) frame: dict[str, Any] = {} - if ignore: - frame["ignore_fields"] = ignore - if plan.mode == "create" or flavor == "found": - frame["target_fields"] = [] - else: - frame["target_fields"] = sorted(target_fields) + frame["target_fields"] = [] if flavor == "found" else sorted(target_fields) if plan.mode == "update": # Another user's save to the target inside the window (a lost # update) shows only in the target's versions. @@ -1681,11 +1951,6 @@ def build_contract( } if nrf: db["new_row_fields"] = nrf - ign = { - t: sorted(c) for t, c in bookkeeping.items() if t not in named_tables and c - } - if ign: - db["ignore_columns"] = ign if settings.recheck_after_s: db["recheck_after_s"] = settings.recheck_after_s frame["database"] = db @@ -2186,6 +2451,23 @@ def check_read_only(connect: Any, dialect: str) -> list[str]: return problems +def read_account_notes(connect: Any, dialect: str) -> list[str]: + """What the read account can do beyond reading, that the contracts + still need (MySQL: TRIGGER, so the whole-database frame sees triggers; + it also lets the account create them).""" + + if dialect != "mysql": + return [] + return ( + [ + "the read account holds TRIGGER (needed to see triggers; it can also " + "create and drop triggers, so keep its password from the agent)" + ] + if _sees_triggers(connect) + else [] + ) + + def guard_problem(sql: str) -> Optional[str]: from openadapt_flow.runtime.effects.sql import assert_read_only_sql @@ -2211,6 +2493,17 @@ def _admin_connect(admin_read: Mapping[str, Any]) -> Any: return module.connect(**args) +def _account(conn: Any, reader: str) -> str: + """``'user'@'host'`` from ``user@host`` or ``'user'@'host'``, each part + escaped by the driver: the account name never reaches SQL as written.""" + + m = re.fullmatch(r"'?([^'@]+)'?(?:@'?([^']*)'?)?", reader.strip()) + if m is None: + raise GenerateError(f"--reader-account {reader!r} is not user@host") + user, host = m.group(1), m.group(2) if m.group(2) is not None else "%" + return f"{conn.escape(user)}@{conn.escape(host)}" + + def grant_trigger(admin_read: Mapping[str, Any], schema: Schema, reader: str) -> str: """MySQL shows a table's triggers only to an account with the TRIGGER privilege, and the whole-database frame must see them (a trigger planted @@ -2218,7 +2511,10 @@ def grant_trigger(admin_read: Mapping[str, Any], schema: Schema, reader: str) -> conn = _admin_connect(admin_read) try: - sql = f"GRANT TRIGGER ON {store.quote('mysql', schema.database)}.* TO {reader}" + sql = ( + f"GRANT TRIGGER ON {store.quote('mysql', schema.database)}.* TO " + f"{_account(conn, reader)}" + ) conn.cursor().execute(sql) conn.commit() finally: @@ -2299,7 +2595,7 @@ def create_views( f"CREATE OR REPLACE SQL SECURITY DEFINER VIEW {v} AS SELECT * FROM {q}" ) made[t] = f"{view_schema(schema)}.oa_read_{t}" - cur.execute(f"GRANT SELECT ON {vs}.* TO {reader}") + cur.execute(f"GRANT SELECT ON {vs}.* TO {_account(conn, reader)}") conn.commit() finally: conn.close() @@ -2400,79 +2696,154 @@ def propose( ) if not open_: answers = {**answers, f"locations.{p.kind}": chosen} + # Q: phone numbers. Comparing by digits is a normalization a person + # confirms per parameter; it is never inferred from a value's shape. + phones = sorted( + { + f"{p.kind}.{name}" + for p in plans + for name, loc in _all_locations(p) + if loc.rule == "phone" + } + ) + if phones: + confirmed = _answer(answers, "phones") + confirmed = set(confirmed) if isinstance(confirmed, list) else set() + questions.append( + Question( + "phones", + "These values were stored as other text with the same digits (a " + "phone number written with spaces stored without): " + + ", ".join(phones) + + ". Compare them as phone numbers (the same digits, at least 7, " + "and a task value that isn't shaped like a phone number is " + "compared exactly)? Answer `phones: [, ...]` with " + "the ones that are phone numbers; any other is compared exactly.", + phones, + ) + ) + for p in plans: + _confirm_phones( + p, {x.split(".", 1)[1] for x in confirmed if x.startswith(p.kind + ".")} + ) + # Q: bookkeeping, for the whole app at once. - logs = set().union(*(p.logs for p in plans)) if plans else set() - book: dict[str, set[str]] = {} + ans = _answer(answers, "bookkeeping") + accepted = ans == "accept" or isinstance(ans, dict) + ans = ans if isinstance(ans, dict) else {} + log_add = set(ans.get("logs_add", [])) + columns_add = {t: set(c) for t, c in dict(ans.get("columns_add", {})).items()} + columns_remove = {t: set(c) for t, c in dict(ans.get("columns_remove", {})).items()} + proposed: dict[str, set[str]] = {} + offered: dict[str, set[str]] = {} + where_seen: dict[str, set[str]] = {} + for p in plans: + for col, kind in p.anchor_extra.items(): + (proposed if kind in BOOKKEEPING_KINDS else offered).setdefault( + p.anchor, set() + ).add(col) + where_seen.setdefault(f"{p.anchor}", set()).add("the target row") + for row_spec in p.touched: + for col, kind in row_spec["columns"].items(): + (proposed if kind in BOOKKEEPING_KINDS else offered).setdefault( + row_spec["table"], set() + ).add(col) + where_seen.setdefault(row_spec["table"], set()).add(row_spec["row"]) + excused: dict[str, set[str]] = {} + if accepted: + for t in set(proposed) | set(columns_add): + excused[t] = (proposed.get(t, set()) | columns_add.get(t, set())) - ( + columns_remove.get(t, set()) + ) unchecked: dict[str, set[str]] = {} for p in plans: - for t, cols in p.bookkeeping.items(): - book.setdefault(t, set()).update(cols) if p.mode == "create" and p.demos: for v in [Variant(p.kind, sorted(p.locations))]: for col, role in _anchor_roles(schema, p, v, actor_params).items(): - if role.role == "varies": + if role.role in ("varies", "free", "timestamp"): unchecked.setdefault(p.anchor, set()).add(col) - log_add: set[str] = set() - log_remove: set[str] = set() - ans = _answer(answers, "bookkeeping") - if isinstance(ans, dict): - log_add, log_remove = ( - set(ans.get("logs_add", [])), - set(ans.get("logs_remove", [])), - ) - logs = (logs | log_add) - log_remove - for t, cols in dict(ans.get("columns_add", {})).items(): - book.setdefault(t, set()).update(cols) - for t, cols in dict(ans.get("columns_remove", {})).items(): - book[t] = book.get(t, set()) - set(cols) + for g in [*p.children.values(), *p.lists.values()]: + for col, role in g.roles.items(): + if role.role in ("varies", "free", "timestamp"): + unchecked.setdefault(g.table, set()).add(col) + candidates = set().union(*(p.log_candidates for p in plans)) if plans else set() + logs = log_add if accepted else set() sides = sorted({f"{s.table} ({s.count} per write)" for p in plans for s in p.sides}) for p in plans: # a table the person calls a log is no longer counted - p.sides = [s for s in p.sides if s.table not in log_add] - if logs or book or unchecked or sides: + p.sides = [x for x in p.sides if x.table not in logs] + p.logs = (candidates & logs) | (p.logs & logs) + unexcused = {t: c for p in plans for t, c in p.unexcused.items()} + if proposed or offered or unchecked or candidates or sides or unexcused: lines = [] + if proposed: + lines.append( + "Proposed as the app's own bookkeeping, excused only on the rows " + "the demonstrations changed (" + + "; ".join( + f"{t}: {', '.join(sorted(where_seen.get(t, set())))}" + for t in sorted(proposed) + ) + + "): " + + "; ".join( + f"{t}: {', '.join(sorted(c))}" for t, c in sorted(proposed.items()) + ) + + "." + ) + if offered: + lines.append( + "Also changed with no task value, and not excused unless you add " + "them under columns_add (a write that changes them stops): " + + "; ".join( + f"{t}: {', '.join(sorted(c))}" for t, c in sorted(offered.items()) + ) + + "." + ) + if unexcused: + lines.append( + "Changed on rows the generator can't identify, so a write that " + "changes them stops: " + + "; ".join( + f"{t}: {', '.join(sorted(c))}" for t, c in sorted(unexcused.items()) + ) + + "." + ) if sides: lines.append( "Rows the app adds beside each write, expected exactly that many " - "times (a second save adds a second one, which stops the write; " - "list a table under logs_add to allow any number): " - + ", ".join(sides) - + "." + "times: " + ", ".join(sides) + "." ) - if logs: + if candidates: lines.append( - "Tables treated as the app's own logs (rows may be added, existing " - "rows must not change" + "Tables that gained a different number of rows in different " + "demonstrations; they are not logs unless you list them under " + "logs_add (rows may then be added, existing rows must not change" + ( ")" if dialect == "mysql" - else "; on PostgreSQL they are not read at all)" + else "; on PostgreSQL a log table is not read at all)" ) + ": " - + ", ".join(sorted(logs)) - ) - if book: - lines.append( - "Columns that change on their own and are not compared: " - + "; ".join( - f"{t}: {', '.join(sorted(c))}" for t, c in sorted(book.items()) if c - ) + + ", ".join(sorted(candidates)) + + "." ) if unchecked: lines.append( - "Columns of a new record that differ between demonstrations and are " - "not checked: " + "Columns of a new record that are not checked (they differ between " + "demonstrations, are free text every demonstration left empty, or " + "are the write's time stamp): " + "; ".join( f"{t}: {', '.join(sorted(c))}" for t, c in sorted(unchecked.items()) ) + + "." ) questions.append( Question( "bookkeeping", - "Is this the app's own bookkeeping, which a write may change without " - "a task asking? " + "Is this the app's own bookkeeping? " + " ".join(lines) - + " Answer `bookkeeping: accept`, or list changes under " - "logs_add, logs_remove, columns_add, columns_remove.", + + " Answer `bookkeeping: accept`, or a mapping with any of " + "logs_add, columns_add, columns_remove. Unanswered, nothing is " + "excused and no table is a log.", "accept", ) ) @@ -2552,6 +2923,7 @@ def propose( } report: dict[str, Any] = { "applied": applied, + "read_account": read_account_notes(connect, dialect), "dialect": dialect, "tables": len(schema.tables), "questions": [ @@ -2597,6 +2969,9 @@ def propose( for s in p.sides ], "logs": sorted(p.logs), + "log_candidates": sorted(p.log_candidates), + "touched_rows": p.touched, + "unexcused": {t: sorted(c) for t, c in p.unexcused.items()}, "refused": list(p.refused), "unresolved": { k: [loc.describe(p.anchor) for loc in v] @@ -2626,8 +3001,9 @@ def propose( continue ok = True for flavor in flavors: - kind_logs = (p.logs | log_add) - log_remove - doc = build_contract(schema, p, v, flavor, settings, book, kind_logs) + doc = build_contract( + schema, p, v, flavor, settings, excused, set(p.logs) + ) rel = Path("contracts") / p.kind / f"{v.slug}.{flavor}.yaml" path = out / rel path.parent.mkdir(parents=True, exist_ok=True) @@ -2742,8 +3118,6 @@ def choose( contract fits: the write is then stopped, never sent unchecked. """ - from openadapt_flow.verifier import Contract - manifest_path = Path(manifest_path) manifest = load_yaml(manifest_path) or {} if manifest.get("schema_version") != MANIFEST_SCHEMA: @@ -2769,10 +3143,10 @@ def choose( base = manifest_path.parent # The record already holds the task's values (found, or an update # already in place): nothing may change. Else the create or update. - found = Contract.load(base / var["found"]) + found = _load_for(base / var["found"], params) if _exists(found, params): return found, params - return Contract.load(base / var[entry["mode"]]), params + return _load_for(base / var[entry["mode"]], params), params raise GenerateError( f"no {kind} contract for the fields {sorted(given)}" + (f" and lists {lists}" if lists else "") @@ -2780,6 +3154,32 @@ def choose( ) +def _load_for(path: Path, params: Mapping[str, Any]) -> Any: + """The contract at ``path`` for these values. A value the contract + compares as a phone number that isn't written like one (fewer than 7 + digits, or other characters) is compared exactly instead.""" + + from openadapt_flow.generate.demo_analysis import phone_shaped + from openadapt_flow.verifier import Contract + + doc = load_yaml(path) or {} + changed = False + for effect in doc.get("effects") or []: + for value in (effect.get("match") or {}).values(): + if ( + isinstance(value, dict) + and value.get("kind") == "phone" + and not phone_shaped(params.get(str(value.get("param")))) + ): + value.pop("kind") + changed = True + if not changed: + return Contract.load(path) + contract = Contract.from_dict(doc, name=Path(path).stem) + contract.config._base_dir = Path(path).resolve().parent + return contract + + def _exists(contract: Any, params: Mapping[str, Any]) -> bool: """Does every effect of the found contract hold now (the record, and each row beside it that holds a value)?""" diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index 62247e50d9..ddda26344c 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -294,7 +294,10 @@ def demo(kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: assert gen.main(argv) == 0 report = json.loads((work / "out" / "report.json").read_text()) # The person accepts the proposals. + # The person accepts the proposals, and says journal_details (one row + # per changed field) is the app's log. answers = {q["key"]: q["proposal"] for q in report["questions"]} + answers["bookkeeping"] = {"logs_add": ["journal_details"]} (work / "answers.yaml").write_text(yaml.safe_dump(answers)) assert gen.main([*argv, "--answers", str(work / "answers.yaml")]) == 0 if os.environ.get("OA_GEN_TEST_KEEP"): @@ -344,7 +347,7 @@ def test_questions_and_contracts(generated: dict[str, Any]) -> None: assert kinds["update_issue"]["emitted"] == 31, json.dumps( kinds["update_issue"], indent=1 ) - assert "journal_details" in kinds["update_issue"]["logs"] + assert "journal_details" in kinds["update_issue"]["log_candidates"] def test_a_correct_create_is_confirmed(generated: dict[str, Any]) -> None: diff --git a/packages/flow/tests/test_generate_verify_contract_attacks.py b/packages/flow/tests/test_generate_verify_contract_attacks.py new file mode 100644 index 0000000000..141d0eebdd --- /dev/null +++ b/packages/flow/tests/test_generate_verify_contract_attacks.py @@ -0,0 +1,263 @@ +# ruff: noqa: F811 (pytest fixtures imported from the generator test, then used as arguments) +"""The adversarial review's attacks on generated contracts (#1672 round 1). + +Each test is one way a wrong write could have passed, from the review, run +on the same throwaway PostgreSQL as ``test_generate_verify_contract``. Every +wrong write must now stop, and a correct update of an issue that already has +history must be CONFIRMED. Records are made up. +""" + +from __future__ import annotations + +import json +import os +from typing import Any + +import pytest +import yaml + +psycopg2 = pytest.importorskip("psycopg2") + +from openadapt_flow.generate import verify_contract as gen # noqa: E402 +from tests.test_generate_verify_contract import ( # noqa: E402,F401 + BOT, + CREATE, + KINDS, + SCHEMA, + App, + _run, + generated, + pg, +) + + +def _fresh_issue(app: App, subject: str) -> int: + return app.create_issue( + {"project": "Help Desk", "subject": subject, "status": "New", + "assignee": None, "due_date": None, "customer_ref": None} + ) # fmt: skip + + +def test_a1_a_number_written_with_the_same_digits_is_stopped( + generated: dict[str, Any], +) -> None: + """The custom field is stored as given, so it is compared exactly; the + phone rule applies only to a value a person confirmed as a phone.""" + + app = generated["app"] + for asked, written in (("1500.75", "15007.5"), ("2026-1104", "202-611-04")): + v = {**CREATE, "subject": f"A1 {asked}", "customer_ref": asked} + out = _run(generated, "create_issue", v, + lambda v=v, w=written: app.create_issue({**v, "customer_ref": w})) # fmt: skip + assert not out.startswith("CONFIRMED"), out + + +def test_a2_a_display_name_two_users_share_is_stopped( + generated: dict[str, Any], +) -> None: + app = generated["app"] + other = app.one( + "INSERT INTO users (login, firstname, lastname) VALUES ('jpark2','Jon','Park') RETURNING id" + ) + try: + v = {**CREATE, "subject": "A2 shared name", "assignee": "Jon Park"} + + def write() -> None: + iid = app.create_issue({**v, "assignee": None}) + app.q("UPDATE issues SET assigned_to_id=%s WHERE id=%s", (other, iid)) + + out = _run(generated, "create_issue", v, write) + assert not out.startswith("CONFIRMED"), out + finally: + app.q("UPDATE issues SET assigned_to_id=NULL WHERE assigned_to_id=%s", (other,)) + app.q("DELETE FROM users WHERE id=%s", (other,)) + + +def test_a3_a_first_and_last_name_split_two_ways_is_stopped( + generated: dict[str, Any], +) -> None: + app = generated["app"] + a = app.one( + "INSERT INTO users (login, firstname, lastname) VALUES ('mal','Mary Ann','Lee') RETURNING id" + ) + b = app.one( + "INSERT INTO users (login, firstname, lastname) VALUES ('mal2','Mary','Ann Lee') RETURNING id" + ) + try: + v = {**CREATE, "subject": "A3 split", "assignee": "Mary Ann Lee"} + + def write() -> None: + iid = app.create_issue({**v, "assignee": None}) + app.q("UPDATE issues SET assigned_to_id=%s WHERE id=%s", (b, iid)) + + out = _run(generated, "create_issue", v, write) + assert not out.startswith("CONFIRMED"), out + finally: + app.q( + "UPDATE issues SET assigned_to_id=NULL WHERE assigned_to_id IN (%s,%s)", + (a, b), + ) + app.q("DELETE FROM users WHERE id IN (%s,%s)", (a, b)) + + +@pytest.mark.xfail( + strict=True, + reason="PostgreSQL has no whole-database frame: a table a person names as a " + "log is not read, so a rewrite of its existing rows is not seen (documented " + "in GENERATE_CONTRACTS.md, Limits)", +) +def test_a4_a_rewrite_of_a_log_table_on_postgresql_is_stopped( + generated: dict[str, Any], +) -> None: + app = generated["app"] + iid = _fresh_issue(app, "A4 fresh") + v = {"project": "Help Desk", "subject": "A4 fresh", "status": "Resolved", "assignee": None, + "due_date": None, "done_ratio": None, "notes": None} # fmt: skip + app.q( + "INSERT INTO journal_details (journal_id, prop_key, old_value, value) VALUES (999,'status_id','1','2')" + ) + + def write() -> None: + app.update_issue(iid, v) + app.q("UPDATE journal_details SET value='999' WHERE journal_id=999") + + out = _run(generated, "update_issue", v, write) + assert not out.startswith("CONFIRMED"), out + + +def test_a5_a_correct_update_of_an_issue_with_history_is_confirmed( + generated: dict[str, Any], +) -> None: + app = generated["app"] + iid = _fresh_issue(app, "A5 history") + first = {"project": "Help Desk", "subject": "A5 history", "status": "In Progress", "assignee": None, + "due_date": None, "done_ratio": None, "notes": None} # fmt: skip + app.update_issue( + iid, first, user=2 + ) # earlier history, by a colleague, before the window + v = {**first, "status": "Resolved"} + out = _run(generated, "update_issue", v, lambda: app.update_issue(iid, v)) + assert out.startswith("CONFIRMED"), out + + +# -- an app that touches a counter row on each create, and soft-deletes ------------- + +SCHEMA_B = ( + SCHEMA.replace( + "lock_version integer NOT NULL DEFAULT 0);", + "lock_version integer NOT NULL DEFAULT 0, deleted smallint NOT NULL DEFAULT 0);", + ) + .replace( + "INSERT INTO settings (name, value) VALUES ('ui_theme', 'classic');", + "INSERT INTO settings (name, value) VALUES ('ui_theme', 'classic'), ('last_issue', '0');", + ) + .replace("CREATE ROLE oa_reader LOGIN PASSWORD 'reader-pw';", "") +) + + +class AppB(App): + def create_issue(self, v: dict[str, Any], *, project_id: Any = None) -> int: + iid = super().create_issue(v, project_id=project_id) + self.q("UPDATE settings SET value=%s WHERE name='last_issue'", (str(iid),)) + return iid + + +@pytest.fixture(scope="module") +def gen_b(pg: Any) -> dict[str, Any]: + root, port = pg["root"], pg["port"] + admin = psycopg2.connect( + host="127.0.0.1", port=port, user="admin", dbname="postgres" + ) + admin.autocommit = True + admin.cursor().execute("CREATE DATABASE trackerb") + admin.close() + app = AppB(port, "trackerb") + with app.con() as c, c.cursor() as cur: + cur.execute( + "DO $$BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='oa_reader') " + "THEN CREATE ROLE oa_reader LOGIN PASSWORD 'reader-pw'; END IF; END$$" + ) + cur.execute(SCHEMA_B) + os.environ["OA_GEN_TEST_PW"] = "reader-pw" + work = root / "workb" + work.mkdir() + read = {"sql_driver": "psycopg2", + "sql_connect_args": {"host": "127.0.0.1", "port": port, "user": "oa_reader", "dbname": "trackerb"}, + "sql_password_env": "OA_GEN_TEST_PW"} # fmt: skip + (work / "read.yaml").write_text(yaml.safe_dump(read)) + kinds = {"kinds": [KINDS["kinds"][0]]} + (work / "kinds.yaml").write_text(yaml.safe_dump(kinds, allow_unicode=True)) + common = ["--read", str(work / "read.yaml")] + for ex in kinds["kinds"][0]["examples"]: + assert gen.main(["snapshot", *common, "--out", str(work / "before.json")]) == 0 + app.create_issue(ex["values"]) + assert gen.main(["record", *common, "--before", str(work / "before.json"), "--kind", "create_issue", + "--values", json.dumps(ex["values"]), "--demos", str(work / "demos")]) == 0 # fmt: skip + argv = ["propose", *common, "--kinds", str(work / "kinds.yaml"), "--demos", str(work / "demos"), + "--out", str(work / "out"), "--actor-param", f"bot_user_id={BOT}"] # fmt: skip + assert gen.main(argv) == 0 + report = json.loads((work / "out" / "report.json").read_text()) + answers = {q["key"]: q["proposal"] for q in report["questions"]} + (work / "answers.yaml").write_text(yaml.safe_dump(answers)) + assert gen.main([*argv, "--answers", str(work / "answers.yaml")]) == 0 + report = json.loads((work / "out" / "report.json").read_text()) + return {"app": app, "out": work / "out", "report": report, "read": read} + + +def test_b0_the_counter_row_is_excused_by_key_and_a_correct_create_passes( + gen_b: dict[str, Any], +) -> None: + rows = gen_b["report"]["kinds"]["create_issue"]["touched_rows"] + assert [(r["table"], r["columns"]) for r in rows] == [ + ("settings", {"value": "counter"}) + ] + app = gen_b["app"] + v = {**CREATE, "subject": "B0 correct"} + out = _run(gen_b, "create_issue", v, lambda: app.create_issue(v)) + assert out.startswith("CONFIRMED"), out + + +def test_b1_another_setting_changed_is_stopped(gen_b: dict[str, Any]) -> None: + app = gen_b["app"] + v = {**CREATE, "subject": "B1"} + try: + out = _run(gen_b, "create_issue", v, lambda: ( + app.create_issue(v), app.q("UPDATE settings SET value='dark' WHERE name='ui_theme'"))) # fmt: skip + finally: + app.q("UPDATE settings SET value='classic' WHERE name='ui_theme'") + assert not out.startswith("CONFIRMED"), out + + +def test_b2_another_records_custom_field_changed_is_stopped( + gen_b: dict[str, Any], +) -> None: + app = gen_b["app"] + victim = app.create_issue({**CREATE, "subject": "victim", "customer_ref": "777"}) + v = {**CREATE, "subject": "B2"} + out = _run(gen_b, "create_issue", v, lambda: ( + app.create_issue(v), + app.q("UPDATE custom_values SET value='HIJACKED' WHERE customized_id=%s", (victim,)))) # fmt: skip + assert not out.startswith("CONFIRMED"), out + + +def test_b3_a_trashed_record_with_the_values_is_not_found( + gen_b: dict[str, Any], +) -> None: + app = gen_b["app"] + v = {**CREATE, "subject": "B3 trashed"} + iid = app.create_issue(v) + app.q("UPDATE issues SET deleted=1 WHERE id=%s", (iid,)) + out = _run(gen_b, "create_issue", v, lambda: None) + assert not out.startswith("CONFIRMED"), out + + +def test_b4_a_create_that_writes_deleted_is_stopped(gen_b: dict[str, Any]) -> None: + app = gen_b["app"] + v = {**CREATE, "subject": "B4"} + + def write() -> None: + iid = app.create_issue(v) + app.q("UPDATE issues SET deleted=1 WHERE id=%s", (iid,)) + + out = _run(gen_b, "create_issue", v, write) + assert not out.startswith("CONFIRMED"), out From 14a6e405aaa4007531e3ea41eea9e974fdf2cf8f Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Fri, 30 Oct 2026 20:26:16 -0400 Subject: [PATCH 38/94] Whole-database frame and recheck on PostgreSQL (#1673) * Whole-database frame and recheck on PostgreSQL frame.database supported only SQLite and MySQL/MariaDB, so on a PostgreSQL app (Redmine in the first-contact study) a record in a table the contract doesn't read, or a write after the verdict, passed. Add a postgresql dialect: one read-only repeatable-read transaction, every column type read by a declared exact rule in a pinned session, tables without a key keyed by the whole row, sequences as counters or compared, and a clear refusal for what can't be read exactly. * Docs: the PostgreSQL frame, measured on Redmine * Frame on PostgreSQL: schemas must be visible, every visible schema read; unambiguous composite keys; large objects; a counter set back shows * Refuse a table or sequence whose name the PostgreSQL snapshot uses itself A table named pg_largeobject in the default schema was replaced by the large-object read and never read (round-2 review SHOULD-FIX). A sequence named like a table had the same collision. Also correct the CHANGELOG's meaning of frame.database.schemas. --- CAPABILITIES.md | 4 +- llms-full.txt | 4 +- packages/flow/CHANGELOG.md | 4 + packages/flow/docs/VERIFY_RUN.md | 90 +- .../runtime/effects/db_snapshot.py | 776 +++++++++++++++++- .../openadapt_flow/runtime/effects/frame.py | 16 +- .../runtime/effects/late_write.py | 4 +- packages/flow/openadapt_flow/verifier.py | 4 +- packages/flow/openadapt_flow/verify_run.py | 5 + .../flow/tests/test_db_snapshot_postgres.py | 743 +++++++++++++++++ 10 files changed, 1604 insertions(+), 46 deletions(-) create mode 100644 packages/flow/tests/test_db_snapshot_postgres.py diff --git a/CAPABILITIES.md b/CAPABILITIES.md index 7bf1771e1e..b8206ee3cf 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -155,8 +155,8 @@ improvising or retrying when something is off. | Check that the saved record matches its source document, not only the parameters: intake keeps each value's line, a second reading must agree before a guarded save, and a round trip compares the read-back with the document | On main | Synthetic (MockMed, text referral) | `openadapt_flow/provenance.py`, `intake`, `verify-run --provenance`, guarded save | | Check one write from Python, without the CLI: one API (`openadapt_flow.verifier`: `begin_write`, `verify_write`; 2.4.1's `capture` form still works, deprecated) with one outcome (`CONFIRMED`, `REFUTED`, `UNCERTAIN`), the receipt, which parts were checked, and the result's scope (read path, stores read, time window, what it doesn't cover) | On main | Synthetic (SQLite; MockMed's fault server over real HTTP) | `openadapt_flow/verifier.py`, [VERIFIER_API.md](packages/flow/docs/VERIFIER_API.md) | | Never send a write twice while its outcome is unknown: an append-only ledger on local disk (pluggable backend), keyed by the write's identity, that every adapter, the MCP tools and the CLI consult before sending; a read-back against the attempt's own baseline resolves it, and a refuted write waits for a person (`openadapt verify ledger list`, `reconcile`, `release`) | On main | Synthetic (MockMed's fault server: a worker killed between the send and the read-back, killed during a timed-out send, killed before the send, a late write, and two worker processes started together on one write) | `openadapt_flow/write_ledger.py`, [VERIFIER_API.md](packages/flow/docs/VERIFIER_API.md#the-uncertain-write-ledger) | -| Catch a write that lands after the verdict (a background job, a trigger, a delayed side record): a `CONFIRMED` with a recheck due is provisional (`provisional`, `recheck_due_at`); the recheck reads the whole database again and refutes the earlier result when a change since the post-save read has no explanation, and a recheck that doesn't confirm holds the write in the ledger. Run it from Python (`verifier.recheck`) or MCP (`verify_recheck`). A write after the recheck's read is not seen (`frame.database.recheck_after_s`, no default window) | On main ([#1639](https://github.com/OpenAdaptAI/OpenAdapt/pull/1639)); provisional result, MCP tool and ledger hold ([#1669](https://github.com/OpenAdaptAI/OpenAdapt/pull/1669)) | Real app (Dolibarr 21 and EspoCRM 10 on MariaDB 11.4, 180 s window: 4 of 4 planted late writes at 125 to 130 s refuted at the recheck, correct writes confirmed; EspoCRM's cron daemon needs its job tables declared); synthetic (SQLite); opt-in live OpenEMR test | `openadapt_flow/verifier.py`, `runtime/effects/late_write.py`, [VERIFY_RUN.md](packages/flow/docs/VERIFY_RUN.md#writes-that-land-later) | -| Diff the whole database for the frame (`frame.database`) on SQLite, MySQL and MariaDB. On MySQL and MariaDB every column type is read exactly (binary strings, `BIT`, geometries and vectors as hex, `FLOAT` widened to `DOUBLE`, `JSON` as stored text, decimals at full precision), so a row the save adds, changes or removes outside the declared records, such as an extra planned call or a stream note, is refuted unless a declared effect, a side record, an attributed concurrent write or an allow-listed bookkeeping table explains it | On main ([#1661](https://github.com/OpenAdaptAI/OpenAdapt/pull/1661)) | Real app (Dolibarr 21 on MariaDB 11.4: all 387 tables read where the frame raised `TypeError`; EspoCRM on MariaDB 11.4: the first-contact study's extra call and stream note refuted, correct opportunities verified); live MariaDB type tests | [VERIFY_RUN.md](packages/flow/docs/VERIFY_RUN.md#the-whole-database), `runtime/effects/db_snapshot.py` | +| Catch a write that lands after the verdict (a background job, a trigger, a delayed side record): a `CONFIRMED` with a recheck due is provisional (`provisional`, `recheck_due_at`); the recheck reads the whole database again and refutes the earlier result when a change since the post-save read has no explanation, and a recheck that doesn't confirm holds the write in the ledger. Run it from Python (`verifier.recheck`) or MCP (`verify_recheck`). A write after the recheck's read is not seen (`frame.database.recheck_after_s`, no default window) | On main ([#1639](https://github.com/OpenAdaptAI/OpenAdapt/pull/1639)); provisional result, MCP tool and ledger hold ([#1669](https://github.com/OpenAdaptAI/OpenAdapt/pull/1669)) | Real app (Dolibarr 21 and EspoCRM 10 on MariaDB 11.4, 180 s window: 4 of 4 planted late writes at 125 to 130 s refuted at the recheck, correct writes confirmed; EspoCRM's cron daemon needs its job tables declared; Redmine 6.1 on PostgreSQL 17, 180 s window: the first-contact study's late writes F19 and F20 refuted at the recheck, correct writes confirmed); synthetic (SQLite); opt-in live OpenEMR test | `openadapt_flow/verifier.py`, `runtime/effects/late_write.py`, [VERIFY_RUN.md](packages/flow/docs/VERIFY_RUN.md#writes-that-land-later) | +| Diff the whole database for the frame (`frame.database`) on SQLite, MySQL, MariaDB and PostgreSQL. On MySQL and MariaDB every column type is read exactly (binary strings, `BIT`, geometries and vectors as hex, `FLOAT` widened to `DOUBLE`, `JSON` as stored text, decimals at full precision). On PostgreSQL every column type is read as text by a declared rule in a session pinned to UTC and exact float output, and a type without a rule, a table the account can't read in full, or an unreadable sequence is refused. So a row the save adds, changes or removes outside the declared records, such as an extra planned call or a stream note, is refuted unless a declared effect, a side record, an attributed concurrent write or an allow-listed bookkeeping table explains it | On main ([#1661](https://github.com/OpenAdaptAI/OpenAdapt/pull/1661)); PostgreSQL ([#1673](https://github.com/OpenAdaptAI/OpenAdapt/pull/1673)) | Real app (Dolibarr 21 on MariaDB 11.4: all 387 tables read where the frame raised `TypeError`; EspoCRM on MariaDB 11.4: the first-contact study's extra call and stream note refuted, correct opportunities verified; Redmine 6.1 on PostgreSQL 17: 58 tables read, a stray upload in a table the contract doesn't read refuted, correct writes verified); live MariaDB and PostgreSQL type tests | [VERIFY_RUN.md](packages/flow/docs/VERIFY_RUN.md#the-whole-database), `runtime/effects/db_snapshot.py` | | Catch a lost update: another user saves a change to the record while the agent works, and the agent saves its stale copy over it. Before and after look like a correct update, so `frame.versions` reads the target's versions (FHIR or SQL version history, a version column, or the app's audit or action-history table) before and after the write. A version by another author, or more than one version step when no author is read, refutes the write; a version that can't be read or attributed stops it | On main ([#1662](https://github.com/OpenAdaptAI/OpenAdapt/pull/1662)) | Real app (HAPI FHIR and EspoCRM, first-contact study fault scripts: both lost updates refuted, correct updates verified, every other case's verdict unchanged) | [VERIFY_RUN.md](packages/flow/docs/VERIFY_RUN.md#lost-updates), `runtime/effects/versions.py` | | Guarded save: a safety layer any agent wraps around one write (check the record, hold for approval, read back) | On main ([#1258](https://github.com/OpenAdaptAI/OpenAdapt/pull/1258)) | Synthetic | `GUARDED_SAVE.md` | | A person's approval in Desktop is signed with a key kept behind Touch ID on a Mac: one-prompt setup in the app, a prompt for every answer, and Flow never sees the key | On main ([#1500](https://github.com/OpenAdaptAI/OpenAdapt/pull/1500)) | Unit tests; opt-in test on a real Secure Enclave | `packages/desktop/openadapt_desktop/approver_keychain.py`, [APPROVAL_MODES.md](packages/flow/docs/APPROVAL_MODES.md#the-approver-key) | diff --git a/llms-full.txt b/llms-full.txt index e060cc35ed..433239090f 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -552,8 +552,8 @@ improvising or retrying when something is off. | Check that the saved record matches its source document, not only the parameters: intake keeps each value's line, a second reading must agree before a guarded save, and a round trip compares the read-back with the document | On main | Synthetic (MockMed, text referral) | `openadapt_flow/provenance.py`, `intake`, `verify-run --provenance`, guarded save | | Check one write from Python, without the CLI: one API (`openadapt_flow.verifier`: `begin_write`, `verify_write`; 2.4.1's `capture` form still works, deprecated) with one outcome (`CONFIRMED`, `REFUTED`, `UNCERTAIN`), the receipt, which parts were checked, and the result's scope (read path, stores read, time window, what it doesn't cover) | On main | Synthetic (SQLite; MockMed's fault server over real HTTP) | `openadapt_flow/verifier.py`, [VERIFIER_API.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFIER_API.md) | | Never send a write twice while its outcome is unknown: an append-only ledger on local disk (pluggable backend), keyed by the write's identity, that every adapter, the MCP tools and the CLI consult before sending; a read-back against the attempt's own baseline resolves it, and a refuted write waits for a person (`openadapt verify ledger list`, `reconcile`, `release`) | On main | Synthetic (MockMed's fault server: a worker killed between the send and the read-back, killed during a timed-out send, killed before the send, a late write, and two worker processes started together on one write) | `openadapt_flow/write_ledger.py`, [VERIFIER_API.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFIER_API.md#the-uncertain-write-ledger) | -| Catch a write that lands after the verdict (a background job, a trigger, a delayed side record): a `CONFIRMED` with a recheck due is provisional (`provisional`, `recheck_due_at`); the recheck reads the whole database again and refutes the earlier result when a change since the post-save read has no explanation, and a recheck that doesn't confirm holds the write in the ledger. Run it from Python (`verifier.recheck`) or MCP (`verify_recheck`). A write after the recheck's read is not seen (`frame.database.recheck_after_s`, no default window) | On main ([#1639](https://github.com/OpenAdaptAI/OpenAdapt/pull/1639)); provisional result, MCP tool and ledger hold ([#1669](https://github.com/OpenAdaptAI/OpenAdapt/pull/1669)) | Real app (Dolibarr 21 and EspoCRM 10 on MariaDB 11.4, 180 s window: 4 of 4 planted late writes at 125 to 130 s refuted at the recheck, correct writes confirmed; EspoCRM's cron daemon needs its job tables declared); synthetic (SQLite); opt-in live OpenEMR test | `openadapt_flow/verifier.py`, `runtime/effects/late_write.py`, [VERIFY_RUN.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFY_RUN.md#writes-that-land-later) | -| Diff the whole database for the frame (`frame.database`) on SQLite, MySQL and MariaDB. On MySQL and MariaDB every column type is read exactly (binary strings, `BIT`, geometries and vectors as hex, `FLOAT` widened to `DOUBLE`, `JSON` as stored text, decimals at full precision), so a row the save adds, changes or removes outside the declared records, such as an extra planned call or a stream note, is refuted unless a declared effect, a side record, an attributed concurrent write or an allow-listed bookkeeping table explains it | On main ([#1661](https://github.com/OpenAdaptAI/OpenAdapt/pull/1661)) | Real app (Dolibarr 21 on MariaDB 11.4: all 387 tables read where the frame raised `TypeError`; EspoCRM on MariaDB 11.4: the first-contact study's extra call and stream note refuted, correct opportunities verified); live MariaDB type tests | [VERIFY_RUN.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFY_RUN.md#the-whole-database), `runtime/effects/db_snapshot.py` | +| Catch a write that lands after the verdict (a background job, a trigger, a delayed side record): a `CONFIRMED` with a recheck due is provisional (`provisional`, `recheck_due_at`); the recheck reads the whole database again and refutes the earlier result when a change since the post-save read has no explanation, and a recheck that doesn't confirm holds the write in the ledger. Run it from Python (`verifier.recheck`) or MCP (`verify_recheck`). A write after the recheck's read is not seen (`frame.database.recheck_after_s`, no default window) | On main ([#1639](https://github.com/OpenAdaptAI/OpenAdapt/pull/1639)); provisional result, MCP tool and ledger hold ([#1669](https://github.com/OpenAdaptAI/OpenAdapt/pull/1669)) | Real app (Dolibarr 21 and EspoCRM 10 on MariaDB 11.4, 180 s window: 4 of 4 planted late writes at 125 to 130 s refuted at the recheck, correct writes confirmed; EspoCRM's cron daemon needs its job tables declared; Redmine 6.1 on PostgreSQL 17, 180 s window: the first-contact study's late writes F19 and F20 refuted at the recheck, correct writes confirmed); synthetic (SQLite); opt-in live OpenEMR test | `openadapt_flow/verifier.py`, `runtime/effects/late_write.py`, [VERIFY_RUN.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFY_RUN.md#writes-that-land-later) | +| Diff the whole database for the frame (`frame.database`) on SQLite, MySQL, MariaDB and PostgreSQL. On MySQL and MariaDB every column type is read exactly (binary strings, `BIT`, geometries and vectors as hex, `FLOAT` widened to `DOUBLE`, `JSON` as stored text, decimals at full precision). On PostgreSQL every column type is read as text by a declared rule in a session pinned to UTC and exact float output, and a type without a rule, a table the account can't read in full, or an unreadable sequence is refused. So a row the save adds, changes or removes outside the declared records, such as an extra planned call or a stream note, is refuted unless a declared effect, a side record, an attributed concurrent write or an allow-listed bookkeeping table explains it | On main ([#1661](https://github.com/OpenAdaptAI/OpenAdapt/pull/1661)); PostgreSQL ([#1673](https://github.com/OpenAdaptAI/OpenAdapt/pull/1673)) | Real app (Dolibarr 21 on MariaDB 11.4: all 387 tables read where the frame raised `TypeError`; EspoCRM on MariaDB 11.4: the first-contact study's extra call and stream note refuted, correct opportunities verified; Redmine 6.1 on PostgreSQL 17: 58 tables read, a stray upload in a table the contract doesn't read refuted, correct writes verified); live MariaDB and PostgreSQL type tests | [VERIFY_RUN.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFY_RUN.md#the-whole-database), `runtime/effects/db_snapshot.py` | | Catch a lost update: another user saves a change to the record while the agent works, and the agent saves its stale copy over it. Before and after look like a correct update, so `frame.versions` reads the target's versions (FHIR or SQL version history, a version column, or the app's audit or action-history table) before and after the write. A version by another author, or more than one version step when no author is read, refutes the write; a version that can't be read or attributed stops it | On main ([#1662](https://github.com/OpenAdaptAI/OpenAdapt/pull/1662)) | Real app (HAPI FHIR and EspoCRM, first-contact study fault scripts: both lost updates refuted, correct updates verified, every other case's verdict unchanged) | [VERIFY_RUN.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFY_RUN.md#lost-updates), `runtime/effects/versions.py` | | Guarded save: a safety layer any agent wraps around one write (check the record, hold for approval, read back) | On main ([#1258](https://github.com/OpenAdaptAI/OpenAdapt/pull/1258)) | Synthetic | `GUARDED_SAVE.md` | | A person's approval in Desktop is signed with a key kept behind Touch ID on a Mac: one-prompt setup in the app, a prompt for every answer, and Flow never sees the key | On main ([#1500](https://github.com/OpenAdaptAI/OpenAdapt/pull/1500)) | Unit tests; opt-in test on a real Secure Enclave | `packages/desktop/openadapt_desktop/approver_keychain.py`, [APPROVAL_MODES.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/APPROVAL_MODES.md#the-approver-key) | diff --git a/packages/flow/CHANGELOG.md b/packages/flow/CHANGELOG.md index c5f1334dc3..879ed736a2 100644 --- a/packages/flow/CHANGELOG.md +++ b/packages/flow/CHANGELOG.md @@ -19,6 +19,8 @@ ### Features +- The whole-database frame (`frame.database`) and its recheck now work on PostgreSQL (`dialect: postgresql`). The snapshot is one read-only repeatable-read transaction over every schema but the system ones. Every column type is read as text by a declared rule, in a session pinned to UTC and exact float output: `numeric` at full precision (with its stored scale when it has no declared one), floats exactly, `bytea` as hex, `json` as stored and `jsonb` canonical, arrays, ranges, enums, domains, `uuid`, network, time, `interval`, `money`, bit, `xml`, text-search and geometric types. A table without a primary key is keyed by the whole row, with duplicates counted. A column type without a rule, a table the account can't read in full (no `SELECT`, row-level security), a foreign table and an unreadable sequence are refused unless the allow-list skips them with a reason. A sequence a column owns is that table's counter, and a counter set back counts as a change; any other sequence is compared as a one-row table. Large objects are read by the SHA-256 of their content. `frame.database.schemas` means the same as on MySQL (each must be visible, and every visible schema is read); a schema is left out only by an `allow_tables` entry `.*` with `mode: skip` and a reason. + - A lost update now stops the run. `frame.versions` names a read of the target's versions (HAPI's `hfj_res_ver`, a version column, or an app's action-history table) and, where the app records one, the author of each version. The verifier reads them before and after the write; a version of a pre-existing target by anyone other than the agent, or more than one version step when no author is read, refutes the frame. A versions read that fails, a target with no version, or a version with no author leaves the frame not checked. The receipt holds counts and a problem code (`evidence.frame.versions`). Contracts without `frame.versions` keep their digests. - A write that lands after the verdict can now be caught. With `frame.database.recheck_after_s`, a verified write's receipt says when a recheck is due (`recheck_due_at`), and `openadapt_flow.verifier.recheck` reads the whole database again at or after that time. It judges every change since the post-save read with the frame's rules: the app profile's bookkeeping, other users' writes the audit log attributes, and changes a later write by the same actor under the same contract saw in its own window (`excused_by`, any outcome). Anything else turns the earlier result into `REFUTED` (late). A change that may be what refuted a later write, a database that can't be read, or a saved state that wasn't issued here or doesn't match its receipt gives `UNCERTAIN`. The saved state holds keyed hashes, not values. `frame.database.settle_attempts` lets the reads after a save, and at a recheck, retry within a stated budget until two in a row match; rows added to append-only tables between them don't count as movement. Without these settings, nothing changes ([#1639](https://github.com/OpenAdaptAI/OpenAdapt/pull/1639)). @@ -27,6 +29,8 @@ ### Fixes +- A composite primary key in the whole-database frame is now kept as a JSON array of its values. Before, the values were joined with `|`, so the keys (`x|y`, `z`) and (`x`, `y|z`) were the same, one row hid the other, and deleting it went unseen. This applies to every dialect. A snapshot or recheck state saved by an earlier version keys rows differently, so recheck a write with the version that checked it. + - The read-only SQL guard now reads the query's tokens instead of matching substrings. `REPLACE(phone, '-', '')`, a `;` inside a string literal, and a table quoted as `` `call` `` no longer refuse the query. Every write and multi-statement form it refused before is still refused. It now also refuses a backslash, a dollar-quoted string, an unterminated literal, and a call to `query_to_xml` or `eval`, which run a query passed as text. When the query has parameters, it also refuses a `%` placeholder inside a literal or quoted name (the driver splices the value in as SQL; `%%` is fine). It refuses a dollar quote right after a number, row locks (`FOR SHARE`, `FOR KEY SHARE`, `LOCK IN SHARE MODE`), and `get_lock`, `pg_advisory_*`, `pg_sleep_for`, `pg_sleep_until`, `master_pos_wait`, `source_pos_wait`, `pg_terminate_backend`, `pg_cancel_backend`, `load_file`, `pg_read_file`, `pg_ls_dir` and `pg_notify`. - The whole-database snapshot reads a SQLite `REAL` with 17 digits, so 0.3 and 0.1+0.2 no longer read the same, and the text `"\x00null"` no longer reads as NULL. A number's plain value, which selectors compare with, is the shortest text that reads back as the stored value (`1.1` for a MySQL `FLOAT`, `1.5` for a `DECIMAL(24,8)`), so a correct write is no longer stopped because a selector stopped matching. diff --git a/packages/flow/docs/VERIFY_RUN.md b/packages/flow/docs/VERIFY_RUN.md index 11da921326..9b4230ca07 100644 --- a/packages/flow/docs/VERIFY_RUN.md +++ b/packages/flow/docs/VERIFY_RUN.md @@ -260,7 +260,7 @@ This is the frame of an EspoCRM "create opportunity" contract on MariaDB: frame: identity_field: id database: - dialect: mysql # or sqlite + dialect: mysql # or postgresql, sqlite read: # read-only: SELECT, TRIGGER and SHOW VIEW sql_driver: pymysql sql_connect_args: {host: 127.0.0.1, port: 3306, user: frame_ro, database: espocrm} @@ -307,6 +307,84 @@ that still can't be read exactly makes the database unreadable, and the run stops; the error names the table. When the database can't be read at the baseline, `begin_write` says why. +### PostgreSQL + +With `dialect: postgresql` (driver `psycopg2` or `psycopg`), the snapshot is +one `REPEATABLE READ, READ ONLY` transaction. The read refuses to go on if the +server reports any other isolation level. + +- **Schemas.** The read covers every schema except `pg_catalog`, + `information_schema` and PostgreSQL's internal ones. A schema the read + account can't see is refused, not left out. `frame.database.schemas` + means what it means on MySQL: each named schema must be visible, and every + other visible schema is still read. Only an `allow_tables` entry + `.*` with `mode: skip` and a reason leaves a schema out; the + receipt lists it under `evidence.frame.database.schemas_skipped`. Leaving + a schema out also leaves out its code, such as a trigger function there + that runs on a table the frame reads. A table in the default schema + (`public`) is named without it (`issues`); a table in another schema is + named with it (`billing.invoice`). +- **The session.** The read pins its session so that each stored value has + one text: `TimeZone` UTC, ISO dates, ISO 8601 intervals, + `extra_float_digits = 3`, bytes as hex, and `lc_monetary` C. A reader + whose own settings differ reads the same thing. +- **Types.** Each column is read as text by a declared rule for its type: + - integers as integers; + - `numeric` at full precision (`NaN` and infinities too). A `numeric` + declared without a scale keeps the scale it was stored with: `1.5` + changed to `1.50` shows, but both still equal the number 1.5; + - `real` and `double precision` exactly, with the shortest text that + reads back as the same float as the plain value; + - `money` as a number; + - `bytea` as the hex of its bytes; + - `json` as its stored text, and `jsonb` as its canonical text; + - as their own text: strings, `uuid`, network addresses, dates, times, + timestamps (in UTC), `interval` (months, days and seconds kept apart, so + `1 day` to `24 hours` shows), bit strings, `xml`, `tsvector`, `tsquery` + and the geometric types; + - arrays, ranges, multiranges, enums and domains by their element, + subtype or base type's rule. + + A composite type, an extension's type, a `reg*` type, and an array of + `money` have no rule. A table with such a column is refused, and the error + names the column. +- **Refusals.** The read also refuses a table the account has no `SELECT` + on, a table whose row-level security hides rows from it, a foreign table, + and a sequence it can't read. A refused table can only be left out by an + `allow_tables` entry with `mode: skip` and a reason. +- **Tables without a primary key.** Their rows are keyed by the whole row, + and identical rows are counted, so a second copy of a row shows as added. +- **Partitions.** A partitioned table is read through its partitions, and + only once. +- **Sequences.** A sequence that a column owns (`serial`, `identity`) is + that table's counter, read just before the snapshot. A row written and + removed in the window moves it, which shows. So does an insert that + failed, because it draws a value too, and a counter set back with + `setval`, after which inserts would reuse numbers. Any other sequence is read as + a one-row table of its own. A value drawn from it is a change. Only an + `allow_tables` entry with `mode: skip` and a reason, which documents it as + the app's bookkeeping, leaves it out. +- **Large objects.** They live in `pg_catalog`, and a column that holds + an object's oid keeps it when the content changes. The read keeps each + object's oid, owner, privileges and the SHA-256 of its content as rows of + a table named `pg_largeobject`, so a changed, added or removed object + shows. An object the account can't read is refused unless `pg_largeobject` + is skipped with a reason. +- **Database code.** The read digests triggers (and whether each is + enabled), views, materialized views, functions, rules, row-level security + policies, event triggers and installed extensions. A change to any of them + in the window shows as `(database code)`. + +The read account needs `USAGE` on each schema and `SELECT` on its tables and +sequences, for example: + +```sql +GRANT USAGE ON SCHEMA public TO frame_ro; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO frame_ro; +GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO frame_ro; +ALTER ROLE frame_ro SET default_transaction_read_only = on; +``` + ## Lost updates Two snapshots can't see a lost update. The agent reads a record, another @@ -639,13 +717,19 @@ such change refutes unless the contract explains it (`allow_tables`, the app profile, `ignore_columns`, `side_records`) or the audit log attributes it to another user. -Measured on two real apps, Dolibarr 21 and EspoCRM 10 on MariaDB 11.4, with -`recheck_after_s: 180`: +Measured on three real apps, Dolibarr 21 and EspoCRM 10 on MariaDB 11.4 and +Redmine 6.1 on PostgreSQL 17, with `recheck_after_s: 180`: - Four planted late writes (a changed phone, a second product, a delayed duplicate opportunity, a look-alike lead's title) landed 125 to 130 s after the save. Each check said `CONFIRMED`, and each recheck said `REFUTED`. Correct writes of the same tasks stayed `CONFIRMED`. +- On Redmine, a time entry's hours changed 150 s after the save, and a + delayed retry created a duplicate issue 140 s after it. Each check said + `CONFIRMED`, and each recheck said `REFUTED`. Correct writes stayed + `CONFIRMED` at the check and the recheck. Redmine's own bookkeeping + needed one declaration: `user_preferences.others` in `ignore_columns` + (every save records the user's last project there). - With the apps' schedulers off, `settle_s: 1` and `settle_attempts: 1` were enough. - With EspoCRM's cron daemon on, its job queue moved during every window. diff --git a/packages/flow/openadapt_flow/runtime/effects/db_snapshot.py b/packages/flow/openadapt_flow/runtime/effects/db_snapshot.py index 93029d104a..3fc813e06d 100644 --- a/packages/flow/openadapt_flow/runtime/effects/db_snapshot.py +++ b/packages/flow/openadapt_flow/runtime/effects/db_snapshot.py @@ -20,16 +20,21 @@ primary key. - **One consistent read.** Everything is read on one connection inside one read-only transaction (``START TRANSACTION WITH CONSISTENT SNAPSHOT, READ - ONLY`` on MySQL and MariaDB; ``BEGIN`` on SQLite). The audit log is read in - the same transaction (``extra_queries``). MySQL's ``AUTO_INCREMENT`` isn't - part of a snapshot, so it is read just before the snapshot starts: a row - committed in between is in the snapshot, never missing from it. + ONLY`` on MySQL and MariaDB; ``BEGIN ISOLATION LEVEL REPEATABLE READ, READ + ONLY`` on PostgreSQL; ``BEGIN`` on SQLite). The audit log is read in the + same transaction (``extra_queries``). MySQL's ``AUTO_INCREMENT`` and + PostgreSQL's sequences aren't part of a snapshot, so they are read just + before the snapshot starts: a row committed in between is in the snapshot, + never missing from it. - **Schema and code.** Each table's definition and the database's triggers, views, routines and events are digested. On MySQL a trigger is visible only to an account with ``TRIGGER``; without it the snapshot says so (``code_visible``), and the frame is not checked. -- **Counters.** ``sqlite_sequence`` and ``AUTO_INCREMENT`` (on the column the - database numbers, key or not) show a row written and deleted in the window. +- **Counters.** ``sqlite_sequence``, ``AUTO_INCREMENT`` and a PostgreSQL + sequence a column owns (on the column the database numbers, key or not) + show a row written and deleted in the window. Any other PostgreSQL sequence + is read as a one-row table of its own, so a value drawn from it is a + change unless the allow-list skips it with a reason. - **The clock.** ``CURRENT_TIMESTAMP`` at the snapshot, so a column the database fills with the time can be held to the window. - **Allow-listed tables** are append-only by default: their existing rows @@ -38,12 +43,16 @@ and MariaDB a row is read as ``JSON_OBJECT``; a type whose value it can't carry as exact text is read another way (:func:`_mysql_encoding`: bytes, bit strings and geometries as hex, ``FLOAT`` widened to ``DOUBLE``, JSON as - its stored text), and numbers parse as ``Decimal``. :func:`canonical` gives - each value one exact text form. + its stored text), and numbers parse as ``Decimal``. On PostgreSQL each + column is read as text by a rule for its type (:func:`_pg_rule`), in a + session pinned to UTC and to exact float output; a type without a rule + makes the table unreadable. :func:`canonical` gives each value one exact + text form. Read-only: SQLite files open with ``mode=ro``; a driver connection should use -an account with SELECT (and, on MySQL, TRIGGER and SHOW VIEW) on every schema -the app can write. The baseline must still be protected like the records: +an account with SELECT (and, on MySQL, TRIGGER and SHOW VIEW; on PostgreSQL, +USAGE on each schema and SELECT on its sequences) on every schema the app can +write. The baseline must still be protected like the records: whoever holds both it and the host secret can test guesses against the MACs. """ @@ -53,6 +62,7 @@ import hashlib import json import os +import re import secrets import struct from collections.abc import Callable, Mapping, Sequence @@ -61,7 +71,7 @@ from pathlib import Path from typing import Any, Literal, Optional -Dialect = Literal["sqlite", "mysql"] +Dialect = Literal["sqlite", "mysql", "postgresql"] TableMode = Literal["compare", "append_only", "skip"] #: Columns per JSON object call: SQLite caps a function at 127 arguments. @@ -157,6 +167,8 @@ def canonical(value: Any) -> str: return "\x00null" if isinstance(value, bool): return "true" if value else "false" + if isinstance(value, StoredNumber): + return value.stored if isinstance(value, (int, float, Decimal)): number = Decimal(str(value)) if not number.is_finite(): @@ -181,6 +193,24 @@ def canonical(value: Any) -> str: return str(value) +class StoredNumber(Decimal): + """A PostgreSQL ``numeric`` declared without a scale, which keeps the + scale it was stored with: ``1.5`` and ``1.50`` are different stored + values. It equals, and prints as, its plain value (``1.5``), so a + selector or parameter compares by value; :func:`canonical` gives the + stored text, so a change of scale alone still shows.""" + + stored: str + + +def _stored_number(text: str) -> StoredNumber: + number = Decimal(text) + exact = Context(prec=max(len(number.as_tuple().digits), 1)) + value = StoredNumber(format(number.normalize(exact), "f")) + value.stored = text + return value + + def _interval_text(value: datetime.timedelta) -> str: micro = (value.days * 86400 + value.seconds) * 1_000_000 + value.microseconds sign, micro = ("-", -micro) if micro < 0 else ("", micro) @@ -307,6 +337,8 @@ class Snapshot: objects: str = "" code_visible: bool = True schemas: list[str] = field(default_factory=list) + #: Schemas the allow-list leaves out (``.*``, skip, with a reason). + schemas_skipped: list[str] = field(default_factory=list) tables: dict[str, Table] = field(default_factory=dict) #: In memory only: rows of ``extra_queries``, read in the same transaction. extra: dict[str, list[dict[str, Any]]] = field(default_factory=dict, repr=False) @@ -320,6 +352,11 @@ def to_json(self) -> dict[str, Any]: "objects": self.objects, "code_visible": self.code_visible, "schemas": self.schemas, + **( + {"schemas_skipped": self.schemas_skipped} + if self.schemas_skipped + else {} + ), "tables": { name: { "mode": t.mode, @@ -349,6 +386,7 @@ def from_json(cls, data: Mapping[str, Any]) -> "Snapshot": objects=str(data.get("objects", "")), code_visible=bool(data.get("code_visible", True)), schemas=list(data.get("schemas", [])), + schemas_skipped=list(data.get("schemas_skipped", [])), tables={ name: Table( mode=t["mode"], @@ -422,6 +460,8 @@ def _default_text(raw: Any, dialect: Dialect) -> Optional[str]: if raw is None: return None + if dialect == "postgresql": + return _pg_default_text(str(raw)) text = str(raw).strip() lowered = text.lower().strip("()") if lowered.startswith( @@ -435,6 +475,40 @@ def _default_text(raw: Any, dialect: Dialect) -> Optional[str]: return text +#: PostgreSQL defaults the database fills with the time (``pg_get_expr``). +_PG_CLOCK = re.compile( + r"(now\(\)|current_timestamp|current_date|localtimestamp|" + r"statement_timestamp\(\)|transaction_timestamp\(\)|clock_timestamp\(\)|" + r"\('now'::text\)::[a-z ]+)(::[a-z ()0-9]+)?", + re.IGNORECASE, +) + + +def _pg_default_text(expr: str) -> Optional[str]: + """A PostgreSQL default (``pg_get_expr``) as the value it stores. + + Only literals are read: ``NULL``, a quoted string with a cast + (``'open'::character varying``), a number (``0``, ``(-1)``) and + ``true``/``false``. A time function is ``CLOCK_DEFAULT``. Any other + expression (``nextval(...)``, ``gen_random_uuid()``) is kept as its + text, which no stored value equals, so a new row that relies on it must + list the column in ``new_row_fields``. + """ + + text = expr.strip() + if _PG_CLOCK.fullmatch(text): + return CLOCK_DEFAULT + if re.fullmatch(r"NULL(::[^']+)?", text, re.IGNORECASE): + return None + m = re.fullmatch(r"'((?:[^']|'')*)'::[^']+", text, re.S) + if m: + return m.group(1).replace("''", "'") + m = re.fullmatch(r"\(?(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\)?(::[^']+)?", text) + if m: + return m.group(1) + return text + + # -- reading ------------------------------------------------------------------ @@ -454,10 +528,19 @@ class _Session: def __init__(self, connect: Callable[[], Any], dialect: Dialect) -> None: self.dialect = dialect self.con = connect() + if dialect == "postgresql": + # psycopg opens a transaction on the first statement unless told + # not to; the snapshot's own BEGIN must be the one that counts. + self.con.autocommit = True self.cur = self.con.cursor() + if dialect == "postgresql": + for sql in _PG_SESSION: + self.cur.execute(sql) def begin(self) -> None: - if self.dialect == "mysql": + if self.dialect == "postgresql": + self.cur.execute("BEGIN ISOLATION LEVEL REPEATABLE READ, READ ONLY") + elif self.dialect == "mysql": # Only statements that can't fail: a driver may end the session # on any error. MySQL 8 caches AUTO_INCREMENT unless told not to; # MariaDB has no such variable. @@ -514,6 +597,16 @@ class _Def: #: The column the database numbers (AUTO_INCREMENT, or SQLite's rowid #: alias), whether or not it is the primary key. auto_column: Optional[str] = None + #: The quoted relation to read (PostgreSQL: ``"schema"."table"``). + relation: str = "" + #: Why the table can't be read exactly (PostgreSQL); it is refused + #: unless the allow-list skips it. + unreadable: str = "" + #: Rows read already (a PostgreSQL sequence's one row), not from a table. + fixed: Optional[list[dict[str, Any]]] = None + #: The SELECT that reads the rows, when it isn't the table's own + #: (PostgreSQL large objects), one text column per ``columns``. + select: str = "" def _digest(parts: Sequence[Any]) -> str: @@ -784,11 +877,568 @@ def _row_part(table: str, chunk: Any) -> dict[str, Any]: return dict(parsed) +# -- PostgreSQL ----------------------------------------------------------------- + +#: The session a PostgreSQL snapshot reads in. Each setting fixes how a type +#: prints, so one stored value has one text: timestamps in UTC (no repeated +#: hour), ISO dates and intervals, floats with every digit they need +#: (shortest exact on PostgreSQL 12 and later, 17 digits before), bytes as +#: hex, ``money`` in the C locale (two decimals, read as ``numeric``). +_PG_SESSION = ( + "SET TimeZone = 'UTC'", + "SET DateStyle = 'ISO, YMD'", + "SET IntervalStyle = 'iso_8601'", + "SET extra_float_digits = 3", + "SET bytea_output = 'hex'", + "SET lc_monetary = 'C'", + "SET client_encoding = 'UTF8'", +) + +#: Schemas a PostgreSQL snapshot never reads: the catalogs, TOAST storage +#: and other sessions' temporary tables. +_PG_SYSTEM_SCHEMAS = ("pg_catalog", "information_schema") + + +def _pg_system(schema: str) -> bool: + return schema in _PG_SYSTEM_SCHEMAS or schema.startswith(("pg_toast", "pg_temp_")) + + +#: Built-in PostgreSQL types (in ``pg_catalog``) and the rule each is read +#: by. Every rule reads the column as text in the pinned session +#: (:data:`_PG_SESSION`), whose output is exact for these types: +#: +#: - ``int``: integers and ``oid``, parsed as ``int``. +#: - ``numeric``: parsed as ``Decimal``, every digit kept (``NaN`` and the +#: infinities too). Declared without a scale, as a :class:`StoredNumber`, +#: which keeps the scale it was stored with. +#: - ``float4``/``float8``: parsed as ``Decimal``; the plain value is the +#: shortest text that reads back as the same stored float. +#: - ``money``: read as ``numeric`` (in the C locale, hundredths). +#: - ``bool``: ``true``/``false`` (its text cast) as ``bool``. +#: - ``bytea``: the hex of the stored bytes, as ``bytes``. +#: - ``text``: the type's own text. Strings, ``uuid``, network addresses, +#: dates and times (UTC), ``interval`` (ISO 8601, months, days and +#: microseconds kept apart), bit strings, ``xml``, ``json`` (its stored +#: text), ``jsonb`` (its canonical text: keys sorted, duplicates gone), +#: ``tsvector`` and ``tsquery`` (sorted lexemes), and the geometric types +#: (their floats printed exactly). Arrays, ranges, multiranges, enums and +#: domains are read by their element, subtype or base type's rule. +_PG_RULES: dict[str, str] = { + **dict.fromkeys(("int2", "int4", "int8", "oid"), "int"), + "numeric": "numeric", + "float4": "float4", + "float8": "float8", + "money": "money", + "bool": "bool", + "bytea": "bytea", + **dict.fromkeys( + ( + "text", + "varchar", + "bpchar", + "name", + "char", + "uuid", + "inet", + "cidr", + "macaddr", + "macaddr8", + "date", + "time", + "timetz", + "timestamp", + "timestamptz", + "interval", + "bit", + "varbit", + "xml", + "json", + "jsonb", + "jsonpath", + "tsvector", + "tsquery", + "point", + "line", + "lseg", + "box", + "path", + "polygon", + "circle", + "pg_lsn", + ), + "text", + ), +} + + +@dataclass +class _PgType: + name: str + schema: str + kind: str # pg_type.typtype: b(ase), d(omain), e(num), r(ange), m(ultirange), c(omposite), p(seudo) + category: str + element: int + base: int + typmod: int = -1 # a domain's own modifier (a numeric's scale) + subtype: int = 0 # a range's subtype, or a multirange's range + + +def _pg_rule( + types: Mapping[int, _PgType], oid: int, typmod: int = -1, depth: int = 0 +) -> str: + """The rule a PostgreSQL column type is read by (:data:`_PG_RULES`). + + Raises ``ValueError`` naming the type when there is none: an extension's + type, a composite, a ``reg*`` alias (it prints by ``search_path``), and + anything else whose text this module hasn't been shown to be exact. + """ + + t = types.get(oid) + if t is None or depth > 8: + raise ValueError(f"type oid {oid} is not in the catalog") + label = f"{t.schema}.{t.name}" + if t.kind == "d": + return _pg_rule(types, t.base, t.typmod, depth + 1) + if t.kind == "e": + return "text" + if t.kind in ("r", "m"): + inner = _pg_rule(types, t.subtype, -1, depth + 1) + if inner == "money": + raise ValueError(f"{label} holds money, which prints by locale") + return "text" + if t.kind == "b" and t.category == "A" and t.element: + inner = _pg_rule(types, t.element, -1, depth + 1) + if inner == "money": + raise ValueError(f"{label} holds money, which prints by locale") + return "text" + if t.kind == "b" and t.schema == "pg_catalog" and t.name in _PG_RULES: + if t.name == "numeric" and typmod == -1: + return "numeric_free" + return _PG_RULES[t.name] + raise ValueError(f"no exact reading rule for type {label}") + + +def _pg_cell(column: str, rule: str) -> str: + q = _quote("postgresql", column) + if rule == "bytea": + return f"encode({q}, 'hex')" + if rule == "money": + return f"({q})::numeric::text" + if rule == "numeric_free": + return f"({q})::text" + return f"({q})::text" + + +def _pg_value(rule: str, text: Optional[str]) -> Any: + """A column's text (read by :func:`_pg_cell`) as its plain value.""" + + if text is None: + return None + if rule == "int": + return int(text) + if rule == "bool": + if text not in ("true", "false"): + raise SnapshotError(f"a boolean read as {text!r}") + return text == "true" + if rule == "bytea": + return bytes.fromhex(text) + if rule == "numeric_free": + return _stored_number(text) + if rule in ("numeric", "money", "float4", "float8"): + number = Decimal(text) + if not number.is_finite(): + return number + if rule == "float4": + return _float32_text(number) + if rule == "float8": + return Decimal(repr(float(number))) + exact = Context(prec=max(len(number.as_tuple().digits), 1)) + return Decimal(format(number.normalize(exact), "f")) + return str(text) + + +def _pg_in(names: Sequence[str]) -> str: + return "(" + ", ".join(_literal(n) for n in names) + ")" + + +@dataclass +class _PgSequence: + name: str + relation: str + increment: int + readable: bool + definition: list[Any] + #: The table and column that own it (``serial``, identity), if any. + table: Optional[str] = None + column: Optional[str] = None + #: The last value drawn (the start minus one step when none was). + last: Optional[int] = None + #: ``last_value`` and ``is_called`` as stored. + raw: Optional[int] = None + is_called: Optional[bool] = None + + +def _pg_scope( + s: _Session, schemas: Sequence[str], skipped: Sequence[str] = () +) -> tuple[list[str], str]: + """The schemas a PostgreSQL snapshot reads, and the default one (whose + tables are named without it). + + Every schema but the system ones is read, as on MySQL. ``schemas`` must + each be visible. A schema is left out only when the allow-list skips it + (``skipped``: ``.*`` with mode ``skip`` and a reason); any other + schema the account can't see is refused rather than left out.""" + + every = { + str(n): bool(u) + for n, u in s.rows( + "SELECT nspname, has_schema_privilege(oid, 'USAGE') FROM pg_namespace" + ) + if not _pg_system(str(n)) + } + unknown = sorted(set(skipped) - set(every)) + if unknown: + raise SnapshotError( + "allow-listed schemas not in the database: " + ", ".join(unknown) + ) + missing = sorted(x for x in schemas if not every.get(x) or x in skipped) + if missing: + raise SnapshotError( + "the read account can't see schema(s): " + ", ".join(missing) + ) + hidden = sorted(n for n, u in every.items() if not u and n not in skipped) + if hidden: + raise SnapshotError( + "the read account can't see schema(s) " + + ", ".join(hidden) + + ": grant it USAGE and SELECT there, or allow-list each as " + "'.*' with mode skip and a reason" + ) + scope = sorted(n for n in every if n not in skipped) + default = str(s.rows("SELECT current_schema()")[0][0] or "") + return scope, default + + +def _pg_name(schema: str, table: str, default: str) -> str: + return table if schema == default else f"{schema}.{table}" + + +def _pg_relation(schema: str, table: str) -> str: + return f"{_quote('postgresql', schema)}.{_quote('postgresql', table)}" + + +def _pg_sequences( + s: _Session, scope: Sequence[str], default: str +) -> dict[str, _PgSequence]: + """Every sequence in scope, with its value. Read before the snapshot + starts (a sequence isn't transactional): a row committed in between is + in the snapshot, so the counter never runs ahead of what it shows.""" + + out: dict[str, _PgSequence] = {} + for row in s.rows( + "SELECT n.nspname, c.relname, s.seqincrement, s.seqstart, s.seqmin, " + "s.seqmax, s.seqcycle, format_type(s.seqtypid, NULL), " + "has_sequence_privilege(c.oid, 'SELECT'), tn.nspname, t.relname, a.attname " + "FROM pg_sequence s JOIN pg_class c ON c.oid = s.seqrelid " + "JOIN pg_namespace n ON n.oid = c.relnamespace " + "LEFT JOIN pg_depend d ON d.classid = 'pg_class'::regclass " + "AND d.objid = c.oid AND d.refclassid = 'pg_class'::regclass " + "AND d.deptype IN ('a', 'i') AND d.refobjsubid > 0 " + "LEFT JOIN pg_class t ON t.oid = d.refobjid " + "LEFT JOIN pg_namespace tn ON tn.oid = t.relnamespace " + "LEFT JOIN pg_attribute a ON a.attrelid = d.refobjid " + "AND a.attnum = d.refobjsubid " + f"WHERE n.nspname IN {_pg_in(scope)} ORDER BY 1, 2" + ): + schema, name, inc, start, low, high, cycle, typ, ok, tschema, table, col = row + out[_pg_name(str(schema), str(name), default)] = _PgSequence( + _pg_name(str(schema), str(name), default), + _pg_relation(str(schema), str(name)), + int(inc), + bool(ok), + [int(inc), int(start), int(low), int(high), bool(cycle), str(typ)], + None if table is None else _pg_name(str(tschema), str(table), default), + None if col is None else str(col), + ) + readable = [q for q in out.values() if q.readable] + if readable: + for name, last, called in s.rows( + " UNION ALL ".join( + f"SELECT {_literal(q.name)}, last_value, is_called FROM {q.relation}" + for q in readable + ) + ): + q = out[str(name)] + q.raw, q.is_called = int(last), bool(called) + q.last = q.raw if called else q.raw - q.increment + return out + + +def _pg_definitions( + s: _Session, scope: Sequence[str], default: str +) -> tuple[dict[str, _Def], str]: + """Every PostgreSQL table's definition (partitions, not their parents), + and the digest of the database's code.""" + + within = _pg_in(scope) + types: dict[int, _PgType] = {} + for oid, name, schema, kind, cat, elem, base, mod in s.rows( + "SELECT t.oid, t.typname, n.nspname, t.typtype, t.typcategory, t.typelem, " + "t.typbasetype, t.typtypmod FROM pg_type t " + "JOIN pg_namespace n ON n.oid = t.typnamespace" + ): + types[int(oid)] = _PgType( + str(name), str(schema), str(kind), str(cat), int(elem), int(base), int(mod) + ) + # pg_range.rngmultitypid exists from PostgreSQL 14; read through JSON so + # an older server gives NULL instead of an error. + for rng, sub, multi in s.rows( + "SELECT r.rngtypid, r.rngsubtype, (to_jsonb(r) ->> 'rngmultitypid')::oid " + "FROM pg_range r" + ): + types[int(rng)].subtype = int(sub) + if multi is not None and int(multi) in types: + types[int(multi)].subtype = int(rng) + tables: dict[int, tuple[str, str, list[Any]]] = {} + out: dict[str, _Def] = {} + parents: list[Any] = [] + for row in s.rows( + "SELECT c.oid, n.nspname, c.relname, c.relkind, c.relpersistence, " + "c.relrowsecurity, c.relforcerowsecurity, has_table_privilege(c.oid, 'SELECT'), " + "CASE WHEN c.relrowsecurity THEN row_security_active(c.oid) ELSE false END " + "FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace " + f"WHERE c.relkind IN ('r', 'p', 'f') AND n.nspname IN {within} ORDER BY 2, 3" + ): + oid, schema, table, kind, persistence, rls, force, select, filtered = row + if kind == "p": + # A partitioned table holds no rows of its own; its partitions + # are read. Its definition is part of the database's code. + parents.append([schema, table]) + continue + name = _pg_name(str(schema), str(table), default) + tables[int(oid)] = (name, str(schema), [kind, persistence, rls, force]) + d = out[name] = _Def( + [], [], None, {}, {}, "", relation=_pg_relation(str(schema), str(table)) + ) + if kind == "f": + d.unreadable = "a foreign table: its rows live outside this database" + elif not select: + d.unreadable = "the read account has no SELECT on it" + elif filtered: + d.unreadable = "row-level security hides rows from the read account" + parts: dict[str, list[Any]] = {n: list(meta) for n, _, meta in tables.values()} + for ( + oid, + column, + typ, + mod, + ctype, + notnull, + cdefault, + identity, + generated, + coll, + ) in s.rows( + "SELECT a.attrelid, a.attname, a.atttypid, a.atttypmod, " + "format_type(a.atttypid, a.atttypmod), " + "a.attnotnull, pg_get_expr(ad.adbin, ad.adrelid), a.attidentity, a.attgenerated, " + "co.collname FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid " + "JOIN pg_namespace n ON n.oid = c.relnamespace " + "LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum " + "LEFT JOIN pg_collation co ON co.oid = a.attcollation " + "WHERE a.attnum > 0 AND NOT a.attisdropped AND c.relkind IN ('r', 'f') " + f"AND n.nspname IN {within} ORDER BY a.attrelid, a.attnum" + ): + if int(oid) not in tables: + continue + name = tables[int(oid)][0] + d = out[name] + d.columns.append(str(column)) + d.defaults[str(column)] = _default_text(cdefault, "postgresql") + try: + d.encode[str(column)] = _pg_rule(types, int(typ), int(mod)) + except ValueError as exc: + d.encode[str(column)] = "" + d.unreadable = d.unreadable or f"column {column}: {exc}" + parts[name].append( + [column, ctype, notnull, cdefault, identity, generated, coll] + ) + for oid, column in s.rows( + "SELECT i.indrelid, a.attname FROM pg_index i " + "CROSS JOIN LATERAL unnest(i.indkey::int2[]) WITH ORDINALITY AS k(attnum, ord) " + "JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum " + "WHERE i.indisprimary ORDER BY i.indrelid, k.ord" + ): + if int(oid) in tables: + out[tables[int(oid)][0]].primary_key.append(str(column)) + for oid, con, kind, definition in s.rows( + "SELECT conrelid, conname, contype, pg_get_constraintdef(oid) " + "FROM pg_constraint WHERE conrelid <> 0 ORDER BY 1, 2" + ): + if int(oid) in tables: + parts[tables[int(oid)][0]].append([con, kind, definition]) + for name, d in out.items(): + d.schema = _digest(parts[name]) + code = [ + parents, + s.rows( + "SELECT n.nspname, c.relname, t.tgname, t.tgenabled, pg_get_triggerdef(t.oid) " + "FROM pg_trigger t JOIN pg_class c ON c.oid = t.tgrelid " + "JOIN pg_namespace n ON n.oid = c.relnamespace " + f"WHERE NOT t.tgisinternal AND n.nspname IN {within} ORDER BY 1, 2, 3" + ), + s.rows( + "SELECT n.nspname, c.relname, c.relkind, pg_get_viewdef(c.oid) " + "FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace " + f"WHERE c.relkind IN ('v', 'm') AND n.nspname IN {within} ORDER BY 1, 2" + ), + s.rows( + "SELECT n.nspname, p.proname, pg_get_function_identity_arguments(p.oid), " + "p.prokind, p.prosrc, p.probin, p.proconfig::text, p.prosecdef, l.lanname " + "FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace " + "JOIN pg_language l ON l.oid = p.prolang " + f"WHERE n.nspname IN {within} ORDER BY 1, 2, 3" + ), + s.rows( + "SELECT schemaname, tablename, rulename, definition FROM pg_rules " + f"WHERE schemaname IN {within} ORDER BY 1, 2, 3" + ), + s.rows( + "SELECT schemaname, tablename, policyname, permissive, roles::text, cmd, " + f"qual, with_check FROM pg_policies WHERE schemaname IN {within} " + "ORDER BY 1, 2, 3" + ), + s.rows( + "SELECT evtname, evtevent, evtenabled, evtfoid::regproc::text, evttags::text " + "FROM pg_event_trigger ORDER BY 1" + ), + s.rows("SELECT extname, extversion FROM pg_extension ORDER BY 1"), + s.rows( + "SELECT n.nspname, t.typname, t.typtype, " + "format_type(t.typbasetype, t.typtypmod), t.typnotnull, t.typdefault, " + "(SELECT string_agg(e.enumlabel, ',' ORDER BY e.enumsortorder) " + "FROM pg_enum e WHERE e.enumtypid = t.oid), " + "(SELECT string_agg(pg_get_constraintdef(c.oid), ';' ORDER BY c.conname) " + "FROM pg_constraint c WHERE c.contypid = t.oid) " + "FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace " + f"WHERE t.typtype IN ('d', 'e', 'r') AND n.nspname IN {within} " + "ORDER BY 1, 2" + ), + ] + return out, _digest(code) + + +def _pg_counters( + defs: dict[str, _Def], sequences: Mapping[str, _PgSequence] +) -> tuple[dict[str, int], list[_PgSequence]]: + """Each table's counter (the sequence its key, or else its first + numbered column, owns), and every other sequence, which is read as a + one-row table of its own.""" + + owned: dict[str, list[_PgSequence]] = {} + for q in sequences.values(): + if q.table in defs and q.column in defs[q.table].columns: + owned.setdefault(q.table, []).append(q) + counters: dict[str, int] = {} + used: set[str] = set() + for table, qs in owned.items(): + d = defs[table] + qs.sort(key=lambda q: d.columns.index(q.column or "")) + pick = next((q for q in qs if d.primary_key == [q.column]), qs[0]) + if pick.increment <= 0: + continue # a descending sequence is read as its own table + d.auto_column = pick.column + if d.primary_key == [pick.column]: + d.generated_key = pick.column + used.add(pick.name) + if pick.last is not None: + counters[table] = pick.last + else: + d.unreadable = d.unreadable or ( + f"the read account has no SELECT on its sequence {pick.name}" + ) + return counters, [q for n, q in sequences.items() if n not in used] + + +def _pg_sequence_def(q: _PgSequence) -> _Def: + """A sequence no table counts by, as a one-row table: a value drawn from + it is a change, like any other row.""" + + columns = ["sequence", "last_value", "is_called"] + d = _Def( + columns, + ["sequence"], + None, + dict.fromkeys(columns), + {}, + _digest(["sequence", *q.definition]), + relation=q.relation, + fixed=[], + ) + if q.raw is None: + d.unreadable = "a sequence the read account has no SELECT on" + else: + d.fixed = [{"sequence": q.name, "last_value": q.raw, "is_called": q.is_called}] + return d + + +#: The name the frame and the allow-list use for PostgreSQL's large objects. +LARGE_OBJECTS = "pg_largeobject" + + +def _pg_large_objects() -> _Def: + """Large objects as one table: each object's oid, owner, privileges and + the SHA-256 of its content. They live in ``pg_catalog``, which is + otherwise not read, and a column that holds an object's oid keeps it when + the content changes (``lo_put``). The content is read only when there + are objects; one the account can't read is refused.""" + + columns = ["oid", "owner", "acl", "sha256"] + return _Def( + columns, + ["oid"], + None, + dict.fromkeys(columns), + dict.fromkeys(columns, "text"), + _digest(["large objects", *columns]), + relation="pg_catalog.pg_largeobject_metadata", + select=( + "SELECT m.oid::text, m.lomowner::regrole::text, m.lomacl::text, " + "encode(sha256(lo_get(m.oid)), 'hex') FROM pg_largeobject_metadata m" + ), + ) + + +def _read_rows_pg(s: _Session, d: _Def) -> list[dict[str, Any]]: + if d.select: + try: + rows = s.rows(d.select) + except Exception as exc: # noqa: BLE001 - e.g. no SELECT on an object + raise SnapshotError( + f"large objects can't be read ({exc}); grant the read account " + f"SELECT on them, or allow-list {LARGE_OBJECTS} with mode skip " + "and a reason" + ) from exc + return [{c: _pg_value("text", v) for c, v in zip(d.columns, r)} for r in rows] + cells = ", ".join(_pg_cell(c, d.encode[c]) for c in d.columns) or "NULL" + return [ + {c: _pg_value(d.encode[c], v) for c, v in zip(d.columns, r)} + for r in s.rows(f"SELECT {cells} FROM ONLY {d.relation}") + ] + + +def key_text(primary_key: Sequence[str], row: Mapping[str, Any]) -> str: + """A keyed row's key as text: a JSON array of its key columns' + canonical values, so (``x|y``, ``z``) and (``x``, ``y|z``) differ.""" + + return json.dumps([canonical(row.get(c)) for c in primary_key]) + + def _row_key( d: _Def, row: Mapping[str, Any], seen: dict[str, int], skip: Sequence[str] = () ) -> str: if d.primary_key: - return "|".join(canonical(row.get(c)) for c in d.primary_key) + return key_text(d.primary_key, row) # No key: the row's contents, without the columns a rule lets move (a # counter row stays the same row when its counter moves). ignored = set(skip) @@ -851,7 +1501,9 @@ def take_snapshot( ignore: Table -> columns left out of each row's contents MAC. ref_columns: Columns attribution and side records read (a record key, a link field): their cell MACs are kept in every table. - schemas: Schemas the read must see (MySQL); refused if one isn't. + schemas: Schemas the read must see; refused if one isn't. Every + visible schema is read either way. On PostgreSQL a schema is left + out only by a ``.*`` entry in ``modes`` with ``skip``. optional_modes: Like ``modes``, for tables an app profile names that this database may not have (a profile covers several versions). ``ignore`` may name ``*`` for every table. @@ -903,18 +1555,54 @@ def _snapshot( dialect = s.dialect counters: dict[str, int] = {} visible = True + sequences: dict[str, _PgSequence] = {} + scope: list[str] = [] + default_schema = "" if dialect == "mysql": # Before the snapshot starts: a row committed after this read is in # the snapshot, so the counter never runs ahead of what it shows. counters = _mysql_counters(s) visible = _sees_triggers(s) + elif dialect == "postgresql": + skipped_schemas = sorted( + t[:-2] for t, m in modes.items() if t.endswith(".*") and m == "skip" + ) + scope, default_schema = _pg_scope(s, schemas, skipped_schemas) + sequences = _pg_sequences(s, scope, default_schema) # before, as above s.begin() - snap = Snapshot( - dialect=dialect, - clock=_clock_text(s.rows("SELECT CURRENT_TIMESTAMP")[0][0]), - code_visible=visible, - ) - defs, snap.objects = _definitions(s) + if dialect == "postgresql": + # The first statement takes the snapshot; its start is the clock. + clock, isolation, read_only = s.rows( + "SELECT statement_timestamp()::text, " + "current_setting('transaction_isolation'), " + "current_setting('transaction_read_only')" + )[0] + if (isolation, read_only) != ("repeatable read", "on"): + raise SnapshotError( + f"the read ran as {isolation}, read_only={read_only}, not one " + "read-only repeatable-read transaction" + ) + else: + clock = s.rows("SELECT CURRENT_TIMESTAMP")[0][0] + snap = Snapshot(dialect=dialect, clock=_clock_text(clock), code_visible=visible) + if dialect == "postgresql": + defs, snap.objects = _pg_definitions(s, scope, default_schema) + counters, others = _pg_counters(defs, sequences) + for q in others: + if q.name in defs: + raise SnapshotError(f"sequence {q.name!r} has the name of a table") + defs[q.name] = _pg_sequence_def(q) + if LARGE_OBJECTS in defs: + # A table of that name would be replaced, and so never read. + raise SnapshotError( + f"a table is named {LARGE_OBJECTS!r}, which the snapshot uses" + ) + defs[LARGE_OBJECTS] = _pg_large_objects() + snap.schemas = list(scope) + snap.schemas_skipped = skipped_schemas + modes = {t: m for t, m in modes.items() if not t.endswith(".*")} + else: + defs, snap.objects = _definitions(s) if dialect == "mysql": default = str(s.rows("SELECT DATABASE()")[0][0]) seen = {default} | {t.split(".", 1)[0] for t in defs if "." in t} @@ -925,7 +1613,7 @@ def _snapshot( ) snap.schemas = sorted(seen) else: - if any( + if dialect == "sqlite" and any( n == "sqlite_sequence" for (n,) in s.rows("SELECT name FROM sqlite_master WHERE type = 'table'") ): @@ -945,17 +1633,30 @@ def _snapshot( for t in sorted(t for t in defs if modes.get(t) == "skip"): d = defs[t] snap.tables[t] = Table("skip", d.schema, d.columns, d.primary_key, None, {}) + refused = sorted(f"{t} ({defs[t].unreadable})" for t in names if defs[t].unreadable) + if refused: + raise SnapshotError( + "can't read exactly: " + + "; ".join(refused) + + ". Skip a table only by allow-listing it with mode skip and a reason" + ) if not names: return snap - counts = { - str(t): int(n) - for t, n in s.rows( - " UNION ALL ".join( - f"SELECT {_literal(t)}, COUNT(*) FROM {_quote(dialect, t)}" - for t in names + + def rel(t: str) -> str: + return defs[t].relation or _quote(dialect, t) + + stored = [t for t in names if defs[t].fixed is None] + counts = {t: len(defs[t].fixed or []) for t in names if defs[t].fixed is not None} + if stored: + counts.update( + (str(t), int(n)) + for t, n in s.rows( + " UNION ALL ".join( + f"SELECT {_literal(t)}, COUNT(*) FROM {rel(t)}" for t in stored + ) ) ) - } numbered = [t for t in names if defs[t].auto_column] maxes = ( { @@ -963,7 +1664,7 @@ def _snapshot( for t, m in s.rows( " UNION ALL ".join( f"SELECT {_literal(t)}, MAX({_quote(dialect, defs[t].auto_column or '')}) " - f"FROM {_quote(dialect, t)}" + f"FROM {rel(t)}" for t in numbered ) ) @@ -1002,7 +1703,15 @@ def _snapshot( table.cell_columns = list(d.columns) skip = [*ignore.get(t, ()), *ignore.get("*", ())] seen_keys: dict[str, int] = {} - for row in _read_rows(s, t, d) if table.count else []: + if d.fixed is not None: + read = d.fixed + elif not table.count: + read = [] + elif dialect == "postgresql": + read = _read_rows_pg(s, d) + else: + read = _read_rows(s, t, d) + for row in read: text = _row_key(d, row, seen_keys, skip) k = row_key_hash(key, text, t) if links is not None and d.primary_key and mode == "compare": @@ -1055,13 +1764,18 @@ def unread_reasons( ): reasons.append("existing rows changed or removed") if new.counter is not None: - # sqlite_sequence holds the last number used; AUTO_INCREMENT the next. + # sqlite_sequence and a PostgreSQL sequence hold the last number + # used; AUTO_INCREMENT the next. step = 1 if dialect == "mysql" else 0 expected = max( old.counter or 0, 0 if new.max_auto is None else new.max_auto + step ) if new.counter > expected: reasons.append("a row was written and removed (counter moved)") + if old.counter is not None and new.counter < old.counter: + # A PostgreSQL sequence can be set back (setval); later inserts + # would then reuse numbers. + reasons.append("the counter went back") return reasons diff --git a/packages/flow/openadapt_flow/runtime/effects/frame.py b/packages/flow/openadapt_flow/runtime/effects/frame.py index cc386f1f46..96ed499086 100644 --- a/packages/flow/openadapt_flow/runtime/effects/frame.py +++ b/packages/flow/openadapt_flow/runtime/effects/frame.py @@ -256,7 +256,8 @@ class AllowedTable(BaseModel): table: str #: ``append_only`` (the default): rows may be added, existing rows must #: not change, and the table needs a generated key. ``skip``: not read at - #: all, which needs a ``reason``. + #: all, which needs a ``reason``. On PostgreSQL ``.*`` names a + #: whole schema, which may only be skipped. mode: Literal["append_only", "skip"] = "append_only" reason: Optional[str] = None @@ -264,6 +265,10 @@ class AllowedTable(BaseModel): def _skip_needs_reason(self) -> "AllowedTable": if self.mode == "skip" and not (self.reason or "").strip(): raise ValueError(f"skipping table {self.table!r} needs a reason") + if self.table.endswith(".*") and self.mode != "skip": + raise ValueError( + f"schema {self.table!r} can only be skipped, with a reason" + ) return self @@ -286,7 +291,7 @@ class FrameDatabase(BaseModel): model_config = ConfigDict(extra="forbid") - dialect: Literal["sqlite", "mysql"] + dialect: Literal["sqlite", "mysql", "postgresql"] #: The read-only connection: ``sqlite_database``, or ``sql_driver`` with #: ``sql_connect_args`` and ``sql_password_env``, as in a ``kind: sql`` #: verifier (no query: every table is read). @@ -307,8 +312,11 @@ class FrameDatabase(BaseModel): #: Over this many rows a table keeps, per row, its key and contents MACs #: and cell MACs only for the columns attribution and side records read. max_rows: int = Field(default=2000, ge=1) - #: Schemas the read account must see (MySQL); the snapshot refuses when - #: one isn't visible, so a schema the app writes can't be left out. + #: Schemas the read account must see; the snapshot refuses when one isn't + #: visible, so a schema the app writes can't be left out. Every other + #: visible schema is read too. On PostgreSQL a schema the account can't + #: see is refused unless ``allow_tables`` skips ``.*`` with a + #: reason. schemas: list[str] = Field(default_factory=list) #: An app profile (``openadapt_flow.app_profiles``): the app's own #: bookkeeping, versioned and shared with rollback and the RL grader. Its diff --git a/packages/flow/openadapt_flow/runtime/effects/late_write.py b/packages/flow/openadapt_flow/runtime/effects/late_write.py index 8b749879dc..080e4e982f 100644 --- a/packages/flow/openadapt_flow/runtime/effects/late_write.py +++ b/packages/flow/openadapt_flow/runtime/effects/late_write.py @@ -261,7 +261,7 @@ def row( from openadapt_flow.runtime.effects.db_snapshot import ( KEY, ROW, - canonical, + key_text, row_hash, row_key_hash, ) @@ -272,7 +272,7 @@ def row( old = self.baseline.tables.get(table) if after is not None: row = {c: v for c, v in after.items() if c not in (KEY, ROW)} - text = "|".join(canonical(row.get(c)) for c in now.primary_key) + text = key_text(now.primary_key, row) k = row_key_hash(self.key, text, table) stored = now.rows.get(k) if stored is None or stored[0] != row_hash(self.key, table, row, skip): diff --git a/packages/flow/openadapt_flow/verifier.py b/packages/flow/openadapt_flow/verifier.py index aaed95f6c3..7547ea85a7 100644 --- a/packages/flow/openadapt_flow/verifier.py +++ b/packages/flow/openadapt_flow/verifier.py @@ -2401,8 +2401,8 @@ def capture_database( from openadapt_flow.verify_run import _db_connect - if dialect not in ("sqlite", "mysql"): - raise VerifierError(f"dialect {dialect!r} is not sqlite or mysql") + if dialect not in ("sqlite", "mysql", "postgresql"): + raise VerifierError(f"dialect {dialect!r} is not sqlite, mysql or postgresql") connect = read if callable(read) else _db_connect(read) extra: dict[str, str] = {} if profile is not None: diff --git a/packages/flow/openadapt_flow/verify_run.py b/packages/flow/openadapt_flow/verify_run.py index 1b0aaffc47..7b82266f5e 100644 --- a/packages/flow/openadapt_flow/verify_run.py +++ b/packages/flow/openadapt_flow/verify_run.py @@ -906,6 +906,11 @@ def _database_frame( 1 for t in new.tables.values() if t.non_transactional ), "schemas": list(new.schemas), + **( + {"schemas_skipped": list(new.schemas_skipped)} + if new.schemas_skipped + else {} + ), "allow_list_sha256": database.allow_list_sha256(), "settle_attempts": used, "settle_attempts_allowed": attempts, diff --git a/packages/flow/tests/test_db_snapshot_postgres.py b/packages/flow/tests/test_db_snapshot_postgres.py new file mode 100644 index 0000000000..c01c3736d7 --- /dev/null +++ b/packages/flow/tests/test_db_snapshot_postgres.py @@ -0,0 +1,743 @@ +"""The whole-database snapshot on PostgreSQL: every column type read exactly. + +The first-contact study ran Redmine on PostgreSQL, where ``frame.database`` +had no dialect, so a write to a table the contract doesn't read, or a write +after the verdict, passed. These tests hold the PostgreSQL dialect to the +MySQL one's bar: one read-only repeatable-read transaction, every column +type read by a declared rule, a one-step change to any column shown as that +column and nothing else, and a clear refusal for what can't be read exactly. + +The rule tests need no database. The live tests run only with +``OPENADAPT_SNAPSHOT_POSTGRES`` set to a libpq DSN for an account that may +create and drop a scratch database and a role, for example:: + + OPENADAPT_SNAPSHOT_POSTGRES='host=127.0.0.1 port=5432 user=postgres + password=... dbname=postgres' \\ + python -m pytest tests/test_db_snapshot_postgres.py -v +""" + +from __future__ import annotations + +import os +import secrets +from decimal import Decimal +from pathlib import Path +from typing import Any, Iterator + +import pytest + +from openadapt_flow.runtime.effects import db_snapshot +from openadapt_flow.runtime.effects.db_snapshot import ( + CLOCK_DEFAULT, + SnapshotError, + StoredNumber, + canonical, + diff, + is_default, + take_snapshot, +) + +# -- rules: no database --------------------------------------------------------- + + +def _types(**extra: db_snapshot._PgType) -> dict[int, db_snapshot._PgType]: + T = db_snapshot._PgType + base = { + 1: T("int4", "pg_catalog", "b", "N", 0, 0), + 2: T("numeric", "pg_catalog", "b", "N", 0, 0), + 3: T("money", "pg_catalog", "b", "N", 0, 0), + 4: T("_int4", "pg_catalog", "b", "A", 1, 0), + 5: T("_money", "pg_catalog", "b", "A", 3, 0), + 6: T("int4range", "pg_catalog", "r", "R", 0, 0, subtype=1), + 7: T("mood", "public", "e", "E", 0, 0), + 8: T("posint", "public", "d", "N", 0, 1), + 9: T("addr", "public", "c", "C", 0, 0), + 10: T("citext", "public", "b", "S", 0, 0), + 11: T("regclass", "pg_catalog", "b", "N", 0, 0), + 12: T("price", "public", "d", "N", 0, 2, typmod=655366), + 13: T("free", "public", "d", "N", 0, 2), + 14: T("bytea", "pg_catalog", "b", "U", 0, 0), + } + return {**base, **extra} + + +@pytest.mark.parametrize( + "oid, typmod, rule", + [ + (1, -1, "int"), + (2, 655366, "numeric"), + (2, -1, "numeric_free"), + (3, -1, "money"), + (4, -1, "text"), + (6, -1, "text"), + (7, -1, "text"), + (8, -1, "int"), + (12, -1, "numeric"), + (13, -1, "numeric_free"), + (14, -1, "bytea"), + ], +) +def test_each_type_has_a_declared_rule(oid: int, typmod: int, rule: str) -> None: + assert db_snapshot._pg_rule(_types(), oid, typmod) == rule + + +@pytest.mark.parametrize( + "oid, message", + [ + (5, "money"), + (9, "public.addr"), + (10, "public.citext"), + (11, "pg_catalog.regclass"), + (99, "not in the catalog"), + ], +) +def test_a_type_without_a_rule_is_refused(oid: int, message: str) -> None: + with pytest.raises(ValueError, match=message): + db_snapshot._pg_rule(_types(), oid) + + +@pytest.mark.parametrize( + "expr, stored", + [ + (None, None), + ("NULL::character varying", None), + ("'open'::character varying", "open"), + ("'O''Brien'::text", "O'Brien"), + ("''::character varying", ""), + ("0", "0"), + ("(-1)", "-1"), + ("'1.50'::numeric", "1.50"), + ("false", "false"), + ("now()", CLOCK_DEFAULT), + ("CURRENT_TIMESTAMP", CLOCK_DEFAULT), + ("('now'::text)::date", CLOCK_DEFAULT), + ("nextval('issues_id_seq'::regclass)", "nextval('issues_id_seq'::regclass)"), + ("gen_random_uuid()", "gen_random_uuid()"), + ], +) +def test_a_default_is_read_only_from_a_literal(expr: Any, stored: Any) -> None: + assert db_snapshot._default_text(expr, "postgresql") == stored + + +def test_a_numeric_without_scale_keeps_it_and_equals_its_value() -> None: + a, b = ( + db_snapshot._pg_value("numeric_free", "1.50"), + db_snapshot._pg_value("numeric_free", "1.5"), + ) + assert isinstance(a, StoredNumber) and a == b == Decimal("1.5") + assert str(a) == "1.5" + assert canonical(a) != canonical(b) + assert is_default(a, "1.5") and is_default( + db_snapshot._pg_value("numeric", "0.00"), "0" + ) + + +@pytest.mark.parametrize( + "rule, text, value", + [ + ("int", "-9223372036854775808", -9223372036854775808), + ("numeric", "12.50", Decimal("12.5")), + ("numeric", "NaN", Decimal("NaN")), + ("float4", "0.1", Decimal("0.1")), + ("float4", "0.100000001", Decimal("0.1")), + ("float8", "0.30000000000000004", Decimal("0.30000000000000004")), + ("float8", "-Infinity", Decimal("-Infinity")), + ("money", "12.30", Decimal("12.3")), + ("bool", "true", True), + ("bytea", "00ff", b"\x00\xff"), + ("text", "P1D", "P1D"), + ], +) +def test_a_value_is_read_by_its_rule(rule: str, text: str, value: Any) -> None: + got = db_snapshot._pg_value(rule, text) + assert canonical(got) == canonical(value) + + +def test_a_boolean_that_isnt_true_or_false_is_refused() -> None: + with pytest.raises(SnapshotError): + db_snapshot._pg_value("bool", "t") + + +# -- a real PostgreSQL ----------------------------------------------------------- + +_DSN = os.environ.get("OPENADAPT_SNAPSHOT_POSTGRES") +live = pytest.mark.skipif( + not _DSN, reason="set OPENADAPT_SNAPSHOT_POSTGRES to a libpq DSN" +) +KEY = b"k" * 32 + +#: A reader whose session prints every type differently from the pinned one: +#: a zone with daylight saving, floats with 6 digits, bytes escaped, SQL +#: dates, Postgres intervals. A pinned read is the same through it. +HOSTILE = ( + "-c TimeZone=America/New_York -c extra_float_digits=0 -c bytea_output=escape " + "-c DateStyle=SQL,DMY -c IntervalStyle=postgres" +) + +#: One column per type, each with a value, and a second value one smallest +#: step away (or differing only where a lossy read would hide it). +COLUMNS: list[tuple[str, str, str, str]] = [ + ("c_int2", "smallint", "-32768", "-32767"), + ("c_int8", "bigint", "9223372036854775807", "9223372036854775806"), + ( + "c_numeric", + "numeric(40,20)", + "12345678901234567890.01234567890123456789", + "12345678901234567890.01234567890123456788", + ), + ("c_numeric_scale", "numeric", "1.5", "1.50"), + ("c_numeric_nan", "numeric", "0", "'NaN'"), + ("c_real", "real", "0.1", "0.10000001"), + ("c_double", "double precision", "1.0000000000000002e300", "1e300"), + ("c_double_zero", "double precision", "0", "'-0'"), + ("c_money", "money", "12.34", "12.35"), + ("c_bool", "boolean", "true", "false"), + ("c_bytea", "bytea", "'\\x00ff'", "'\\x00fe'"), + ("c_char", "char(4)", "'ab'", "'ac'"), + ("c_varchar", "varchar(20)", "'a '", "'a'"), + ("c_text", "text", "'x'", "'X'"), + ( + "c_uuid", + "uuid", + "'00000000-0000-0000-0000-000000000001'", + "'00000000-0000-0000-0000-000000000002'", + ), + ("c_inet", "inet", "'10.0.0.1'", "'10.0.0.1/24'"), + ("c_cidr", "cidr", "'10.0.0.0/24'", "'10.0.0.0/25'"), + ("c_macaddr", "macaddr", "'08:00:2b:01:02:03'", "'08:00:2b:01:02:04'"), + ("c_date", "date", "'2026-01-02'", "'2026-01-03'"), + ("c_time", "time(6)", "'03:04:05.000001'", "'03:04:05'"), + ("c_timetz", "timetz", "'03:04:05+01'", "'03:04:05+02'"), + ( + "c_timestamp", + "timestamp(6)", + "'2026-01-02 03:04:05.6'", + "'2026-01-02 03:04:05.600001'", + ), + ( + "c_timestamptz", + "timestamptz", + "'2026-11-01 05:30:00+00'", + "'2026-11-01 06:30:00+00'", + ), + ("c_interval", "interval", "'1 day'", "'24 hours'"), + ("c_bit", "bit(5)", "B'10101'", "B'10100'"), + ("c_varbit", "varbit(8)", "B'1'", "B'10'"), + ("c_json", "json", "'{\"b\": 1.10}'", "'{\"b\": 1.1}'"), + ("c_jsonb", "jsonb", "'{\"b\": 1.10}'", "'{\"b\": 1.1}'"), + ("c_xml", "xml", "'1'", "'2'"), + ("c_tsvector", "tsvector", "'a:1 b:2'", "'a:1 b:3'"), + ("c_tsquery", "tsquery", "'a & b'", "'a | b'"), + ("c_point", "point", "'(1,2)'", "'(1,2.0000000000000004)'"), + ("c_line", "line", "'{1,2,3}'", "'{1,2,4}'"), + ("c_lseg", "lseg", "'[(0,0),(1,1)]'", "'[(0,0),(1,2)]'"), + ("c_box", "box", "'(1,1),(0,0)'", "'(2,1),(0,0)'"), + ("c_path", "path", "'[(0,0),(1,1)]'", "'((0,0),(1,1))'"), + ("c_polygon", "polygon", "'((0,0),(1,1),(1,0))'", "'((0,0),(1,1),(2,0))'"), + ("c_circle", "circle", "'<(0,0),1>'", "'<(0,0),2>'"), + ("c_int_array", "int[]", "'{1,2}'", "'{2,1}'"), + ("c_float_array", "float8[]", "'{0.1}'", "'{0.30000000000000004}'"), + ("c_real_array", "real[]", "'{0.1}'", "'{0.10000001}'"), + ("c_text_array", "text[]", "'{\"a b\"}'", "'{a,b}'"), + ("c_bytea_array", "bytea[]", "ARRAY['\\x00'::bytea]", "ARRAY['\\x0000'::bytea]"), + ("c_int4range", "int4range", "'[1,5)'", "'[1,6)'"), + ( + "c_tstzrange", + "tstzrange", + "'[2026-01-01 00:00+00,2026-01-02 00:00+00)'", + "'[2026-01-01 00:00+00,2026-01-02 00:00:00.000001+00)'", + ), + ("c_multirange", "int4multirange", "'{[1,3)}'", "'{[1,3),[4,5)}'"), + ("c_enum", "mood", "'x'", "'y'"), + ("c_domain", "posint", "1", "2"), +] + + +class Pg: + def __init__(self, dsn: str) -> None: + import psycopg2 + + self.psycopg2 = psycopg2 + self.dsn = dsn + self.name = "openadapt_snapshot_pg_" + secrets.token_hex(4) + self.reader = "openadapt_snapshot_ro_" + secrets.token_hex(4) + self.password = secrets.token_hex(16) + + def admin(self, dbname: str | None = None) -> Any: + extra = {} if dbname is None else {"dbname": dbname} + con = self.psycopg2.connect(self.dsn, **extra) + con.autocommit = True + return con + + def sql(self, *statements: str) -> None: + con = self.admin(self.name) + with con.cursor() as cur: + for statement in statements: + cur.execute(statement) + con.close() + + def connect(self, options: str = "", reader: bool = False) -> Any: + args: dict[str, Any] = {"dbname": self.name} + if options: + args["options"] = options + if reader: + args.update(user=self.reader, password=self.password) + return lambda: self.psycopg2.connect(self.dsn, **args) + + def snap(self, before: Any = None, options: str = "", **kw: Any) -> Any: + return take_snapshot( + self.connect(options, kw.pop("reader", False)), + "postgresql", + key=KEY, + before=before, + **kw, + ) + + +@pytest.fixture +def pg() -> Iterator[Pg]: + db = Pg(_DSN or "") + con = db.admin() + with con.cursor() as cur: + cur.execute(f"CREATE DATABASE {db.name}") + cur.execute(f"CREATE ROLE {db.reader} LOGIN PASSWORD '{db.password}'") + cols = ", ".join(f"{c} {t}" for c, t, _, _ in COLUMNS) + names = ", ".join(c for c, *_ in COLUMNS) + values = ", ".join(v for _, _, v, _ in COLUMNS) + db.sql( + "CREATE TYPE mood AS ENUM ('x', 'y')", + "CREATE DOMAIN posint AS integer CHECK (VALUE > 0)", + f"CREATE TABLE t (id serial PRIMARY KEY, {cols})", + f"INSERT INTO t ({names}) VALUES ({values})", + f"INSERT INTO t ({names}) VALUES ({values})", + "INSERT INTO t (id) VALUES (DEFAULT)", + "CREATE TABLE pairs (a int, b text)", + "INSERT INTO pairs VALUES (1, 'x'), (1, 'x'), (2, 'y')", + f"GRANT USAGE ON SCHEMA public TO {db.reader}", + f"GRANT SELECT ON ALL TABLES IN SCHEMA public TO {db.reader}", + f"GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO {db.reader}", + ) + try: + yield db + finally: + with con.cursor() as cur: + cur.execute(f"DROP DATABASE {db.name} WITH (FORCE)") + cur.execute(f"DROP ROLE {db.reader}") + con.close() + + +@live +def test_every_type_is_read_and_a_reread_is_equal(pg: Pg) -> None: + a = pg.snap() + assert a.tables["t"].count == 3 and a.schemas == ["public"] + assert a.tables["t"].counter == 3 and a.tables["t"].generated_key == "id" + assert diff(a, pg.snap(before=a), key=KEY) == {} + + +@live +def test_a_reader_session_set_otherwise_reads_the_same(pg: Pg) -> None: + a = pg.snap() + assert diff(a, pg.snap(before=a, options=HOSTILE), key=KEY) == {} + + +@live +@pytest.mark.parametrize( + "column, _type, _old, new", COLUMNS, ids=[c[0] for c in COLUMNS] +) +def test_a_one_step_change_to_any_column_shows( + pg: Pg, column: str, _type: str, _old: str, new: str +) -> None: + a = pg.snap(options=HOSTILE) + pg.sql(f"UPDATE t SET {column} = {new} WHERE id = 1") + b = pg.snap(before=a, options=HOSTILE) + moved = diff(a, b, key=KEY) + assert list(moved) == ["t"] + assert (moved["t"].changed, moved["t"].changed_columns) == (1, [column]) + assert b.tables["t"].values, "the changed row's plain values are kept" + + +@live +@pytest.mark.parametrize( + "column, _type, old, _new", COLUMNS, ids=[c[0] for c in COLUMNS] +) +def test_a_null_and_a_value_differ( + pg: Pg, column: str, _type: str, old: str, _new: str +) -> None: + a = pg.snap() + pg.sql(f"UPDATE t SET {column} = {old} WHERE id = 3") + moved = diff(a, pg.snap(before=a), key=KEY) + assert (moved["t"].changed, moved["t"].changed_columns) == (1, [column]) + + +@live +def test_plain_values_read_as_people_write_them(pg: Pg) -> None: + a = pg.snap() + pg.sql("UPDATE t SET c_int2 = 7 WHERE id = 1") + row = next(iter(pg.snap(before=a).tables["t"].values.values())) + assert str(row["c_real"]) == "0.1" and str(row["c_numeric_scale"]) == "1.5" + assert str(row["c_money"]) == "12.34" and row["c_bool"] is True + assert row["c_bytea"] == b"\x00\xff" and row["c_interval"] == "P1D" + assert row["c_timestamptz"] == "2026-11-01 05:30:00+00" + + +@live +def test_a_table_without_a_key_counts_duplicate_rows(pg: Pg) -> None: + a = pg.snap() + assert a.tables["pairs"].count == 3 and not a.tables["pairs"].primary_key + pg.sql("INSERT INTO pairs VALUES (1, 'x')") + b = pg.snap(before=a) + assert ( + diff(a, b, key=KEY)["pairs"].added, + diff(a, b, key=KEY)["pairs"].removed, + ) == (1, 0) + pg.sql( + "DELETE FROM pairs WHERE ctid IN (SELECT ctid FROM pairs WHERE a = 1 LIMIT 2)" + ) + moved = diff(a, pg.snap(before=a), key=KEY)["pairs"] + assert (moved.added, moved.removed) == (0, 1) + + +@live +def test_a_row_written_and_removed_moves_the_counter(pg: Pg) -> None: + a = pg.snap() + pg.sql("INSERT INTO t (id) VALUES (DEFAULT)", "DELETE FROM t WHERE id = 4") + moved = diff(a, pg.snap(before=a), key=KEY) + assert moved["t"].unread == ["a row was written and removed (counter moved)"] + + +@live +def test_a_sequence_no_table_owns_is_compared_unless_skipped(pg: Pg) -> None: + pg.sql("CREATE SEQUENCE invoice_no", f"GRANT SELECT ON invoice_no TO {pg.reader}") + a = pg.snap(reader=True) + pg.sql("SELECT nextval('invoice_no')") + moved = diff(a, pg.snap(before=a, reader=True), key=KEY) + assert list(moved) == ["invoice_no"] and moved["invoice_no"].changed == 1 + skip = {"invoice_no": "skip"} + a = pg.snap(reader=True, modes=skip) + pg.sql("SELECT nextval('invoice_no')") + assert diff(a, pg.snap(before=a, reader=True, modes=skip), key=KEY) == {} + + +@live +def test_a_column_without_a_rule_is_refused_unless_skipped(pg: Pg) -> None: + pg.sql( + "CREATE TYPE addr AS (street text, n int)", + "CREATE TABLE places (id int PRIMARY KEY, a addr, m money[])", + ) + with pytest.raises( + SnapshotError, match=r"places \(column a: no exact reading rule" + ): + pg.snap() + a = pg.snap(modes={"places": "skip"}) + assert a.tables["places"].mode == "skip" + + +@live +def test_what_the_reader_cant_see_is_refused(pg: Pg) -> None: + pg.sql("CREATE TABLE secret (id int PRIMARY KEY)") + with pytest.raises( + SnapshotError, + match="secret .no SELECT|secret \\(the read account has no SELECT", + ): + pg.snap(reader=True) + pg.sql( + f"GRANT SELECT ON secret TO {pg.reader}", + "CREATE SCHEMA hidden", + "CREATE TABLE hidden.h (id int PRIMARY KEY)", + ) + with pytest.raises( + SnapshotError, match="can't see schema.s. hidden.*'.\\*'" + ): + pg.snap(reader=True) + # Naming schemas doesn't narrow the read (review #1673, B1): a hidden + # schema is still refused. + with pytest.raises(SnapshotError, match="can't see schema.s. hidden"): + pg.snap(reader=True, schemas=["public"]) + with pytest.raises(SnapshotError, match="can't see schema.s.: hidden"): + pg.snap(reader=True, schemas=["public", "hidden"]) + # Only the allow-list leaves a schema out. + a = pg.snap(reader=True, modes={"hidden.*": "skip"}) + assert a.schemas == ["public"] and a.schemas_skipped == ["hidden"] + assert "hidden.h" not in a.tables + with pytest.raises(SnapshotError, match="allow-listed schemas not in the database"): + pg.snap(reader=True, modes={"nosuch.*": "skip"}) + + +@live +def test_named_schemas_still_read_every_visible_schema(pg: Pg) -> None: + # Review #1673, B1: with schemas=["public"], a stray row in another + # schema and a rewritten trigger function in a third both show. + pg.sql( + "CREATE SCHEMA other", + "CREATE TABLE other.note (id serial PRIMARY KEY, body text)", + "CREATE SCHEMA util", + "CREATE FUNCTION util.f() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RETURN NEW; END'", + "CREATE TRIGGER tr BEFORE INSERT ON pairs FOR EACH ROW EXECUTE FUNCTION util.f()", + ) + a = pg.snap(schemas=["public"]) + assert a.schemas == ["other", "public", "util"] + pg.sql("INSERT INTO other.note (body) VALUES ('stray')") + assert ( + diff(a, pg.snap(before=a, schemas=["public"]), key=KEY)["other.note"].added == 1 + ) + b = pg.snap(schemas=["public"]) + pg.sql( + "CREATE OR REPLACE FUNCTION util.f() RETURNS trigger LANGUAGE plpgsql " + "AS 'BEGIN NEW.b := ''x''; RETURN NEW; END'" + ) + assert "(database code)" in diff(b, pg.snap(before=b, schemas=["public"]), key=KEY) + + +def test_sqlite_composite_keys_that_join_alike_stay_apart(tmp_path: Path) -> None: + # Review #1673, S1, on every dialect: the key is a JSON array. + import sqlite3 + + path = tmp_path / "kv.sqlite" + con = sqlite3.connect(path) + con.executescript( + "CREATE TABLE kv (a TEXT, b TEXT, PRIMARY KEY (a, b));" + "INSERT INTO kv VALUES ('x|y', 'z'), ('x', 'y|z');" + ) + con.commit() + + def connect() -> Any: + return sqlite3.connect(f"file:{path}?mode=ro", uri=True) + + a = take_snapshot(connect, "sqlite", key=KEY) + assert len(a.tables["kv"].rows) == 2 + con.execute("DELETE FROM kv WHERE a = 'x|y'") + con.commit() + assert ( + diff(a, take_snapshot(connect, "sqlite", key=KEY, before=a), key=KEY)[ + "kv" + ].removed + == 1 + ) + + +def test_a_schema_entry_in_the_allow_list_can_only_be_skipped() -> None: + from openadapt_flow.runtime.effects.frame import AllowedTable + + AllowedTable(table="audit.*", mode="skip", reason="the DBA's own audit trail") + with pytest.raises(ValueError, match="can only be skipped"): + AllowedTable(table="audit.*") + + +@live +def test_composite_keys_that_join_alike_stay_apart(pg: Pg) -> None: + # Review #1673, S1: ('x|y', 'z') and ('x', 'y|z') are two rows. + pg.sql( + "CREATE TABLE kv (a text, b text, PRIMARY KEY (a, b))", + "INSERT INTO kv VALUES ('x|y', 'z'), ('x', 'y|z')", + ) + a = pg.snap() + assert len(a.tables["kv"].rows) == 2 + pg.sql("DELETE FROM kv WHERE a = 'x|y'") + assert diff(a, pg.snap(before=a), key=KEY)["kv"].removed == 1 + + +@live +def test_large_objects_are_read_by_content_or_skipped(pg: Pg) -> None: + # Review #1673, S2: lo_put keeps the oid but changes the content. + pg.sql("SELECT lo_from_bytea(424242, 'scan v1')") + a = pg.snap() + assert a.tables["pg_largeobject"].count == 1 + pg.sql("SELECT lo_put(424242, 0, 'SCAN')") + moved = diff(a, pg.snap(before=a), key=KEY) + assert list(moved) == ["pg_largeobject"] + assert moved["pg_largeobject"].changed_columns == ["sha256"] + b = pg.snap() + pg.sql("SELECT lo_unlink(424242)") + assert diff(b, pg.snap(before=b), key=KEY)["pg_largeobject"].removed == 1 + pg.sql("SELECT lo_from_bytea(424243, 'x')") + with pytest.raises(SnapshotError, match="large objects can't be read"): + pg.snap(reader=True) + skip = {"pg_largeobject": "skip"} + c = pg.snap(reader=True, modes=skip) + pg.sql("SELECT lo_put(424243, 0, 'y')") + assert diff(c, pg.snap(before=c, reader=True, modes=skip), key=KEY) == {} + + +@live +def test_a_counter_set_back_shows(pg: Pg) -> None: + # Review #1673, N1: setval back lets later inserts reuse ids. + a = pg.snap() + pg.sql("SELECT setval('t_id_seq', 1)") + moved = diff(a, pg.snap(before=a), key=KEY) + assert moved["t"].unread == ["the counter went back"] + + +@live +def test_row_level_security_that_hides_rows_is_refused(pg: Pg) -> None: + pg.sql( + "ALTER TABLE pairs ENABLE ROW LEVEL SECURITY", + f"CREATE POLICY only_one ON pairs FOR SELECT TO {pg.reader} USING (a = 1)", + ) + with pytest.raises(SnapshotError, match="row-level security"): + pg.snap(reader=True) + + +@live +def test_another_schema_is_read_and_named_with_it(pg: Pg) -> None: + pg.sql( + "CREATE SCHEMA billing", + "CREATE TABLE billing.invoice (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, n int)", + "INSERT INTO billing.invoice (n) VALUES (1)", + ) + a = pg.snap() + assert a.schemas == ["billing", "public"] + assert a.tables["billing.invoice"].counter == 1 + pg.sql("INSERT INTO billing.invoice (n) VALUES (2)") + assert diff(a, pg.snap(before=a), key=KEY)["billing.invoice"].added == 1 + + +@live +def test_a_partitioned_table_is_read_once_through_its_partitions(pg: Pg) -> None: + pg.sql( + "CREATE TABLE ev (id int, at date) PARTITION BY RANGE (at)", + "CREATE TABLE ev_2026 PARTITION OF ev FOR VALUES FROM ('2026-01-01') TO ('2027-01-01')", + "INSERT INTO ev VALUES (1, '2026-05-01')", + ) + a = pg.snap() + assert "ev" not in a.tables and a.tables["ev_2026"].count == 1 + + +@live +def test_a_trigger_planted_in_the_window_shows(pg: Pg) -> None: + a = pg.snap() + pg.sql( + "CREATE FUNCTION f() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RETURN NEW; END'", + "CREATE TRIGGER tr BEFORE INSERT ON pairs FOR EACH ROW EXECUTE FUNCTION f()", + ) + b = pg.snap(before=a) + assert "(database code)" in diff(a, b, key=KEY) + pg.sql("ALTER TABLE pairs DISABLE TRIGGER tr") + assert "(database code)" in diff(b, pg.snap(before=b), key=KEY) + + +@live +@pytest.mark.parametrize( + "change", + [ + "ALTER TYPE mood ADD VALUE 'z'", + "ALTER DOMAIN posint ADD CONSTRAINT small CHECK (VALUE < 1000)", + "CREATE RULE r AS ON INSERT TO pairs DO INSTEAD NOTHING", + "CREATE VIEW v AS SELECT a FROM pairs", + ], +) +def test_a_change_to_types_rules_or_views_shows(pg: Pg, change: str) -> None: + a = pg.snap() + pg.sql(change) + assert "(database code)" in diff(a, pg.snap(before=a), key=KEY) + + +@live +def test_a_column_default_is_read_from_the_catalog(pg: Pg) -> None: + pg.sql( + "CREATE TABLE d (id serial PRIMARY KEY, s varchar(9) DEFAULT 'open', " + "n numeric(6,2) DEFAULT 0, at timestamptz DEFAULT now(), f boolean DEFAULT false)" + ) + defaults = pg.snap().tables["d"].defaults + assert defaults == { + "id": db_snapshot.ANY_VALUE, + "s": "open", + "n": "0", + "at": CLOCK_DEFAULT, + "f": "false", + } + + +# -- the frame and its recheck, end to end ---------------------------------------- + + +@live +def test_the_frame_refutes_a_late_write_and_an_extra_record_on_postgresql( + pg: Pg, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from psycopg2.extensions import parse_dsn + + from openadapt_flow import verifier + from openadapt_flow.verifier import ( + Actor, + Contract, + begin_write, + recheck, + verify_write, + ) + + monkeypatch.setenv("OPENADAPT_LEDGER_DIR", str(tmp_path / "ledger")) + monkeypatch.setenv("OPENADAPT_SEAL_HOME", str(tmp_path / "seal")) + monkeypatch.setattr(verifier, "USED_DIR", tmp_path / "used") + monkeypatch.setattr(verifier, "seconds_until", lambda due: 0.0) + monkeypatch.setenv("PG_READER_PW", pg.password) + pg.sql( + "CREATE TABLE contact (id serial PRIMARY KEY, name text, email text)", + "CREATE TABLE note (id serial PRIMARY KEY, body text)", + f"GRANT SELECT ON ALL TABLES IN SCHEMA public TO {pg.reader}", + f"GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO {pg.reader}", + "CREATE SCHEMA other", + "CREATE TABLE other.note (id serial PRIMARY KEY, body text)", + f"GRANT USAGE ON SCHEMA other TO {pg.reader}", + f"GRANT SELECT ON ALL TABLES IN SCHEMA other TO {pg.reader}", + f"GRANT SELECT ON ALL SEQUENCES IN SCHEMA other TO {pg.reader}", + ) + dsn = parse_dsn(pg.dsn) + read = { + "sql_driver": "psycopg2", + "sql_connect_args": { + "host": dsn.get("host", "127.0.0.1"), + "port": int(dsn.get("port", 5432)), + "user": pg.reader, + "dbname": pg.name, + }, + "sql_password_env": "PG_READER_PW", + } + contract = Contract.from_dict( + { + "schema_version": "openadapt.verify/v1", + "verifier": {"kind": "sql", **read, "sql_query": "SELECT * FROM contact"}, + "record": { + "name": "contact", + "match": ["email"], + "fields": {"name": "name", "email": "email"}, + }, + "frame": { + "identity_field": "id", + "database": { + "dialect": "postgresql", + "read": read, + "views": {"contact": ""}, + "settle_s": 0, + "recheck_after_s": 60, + # Must be visible; every other visible schema is read too. + "schemas": ["public"], + }, + }, + } + ) + bot = Actor(kind="agent", id="bot") + + def save(n: int, *stray: str) -> Any: + params = {"name": "Ada", "email": f"ada{n}@example.test"} + before = begin_write(contract, params, actor=bot) + pg.sql( + f"INSERT INTO contact (name, email) VALUES ('Ada', 'ada{n}@example.test')", + *stray, + ) + return params, verify_write(before) + + params, ok = save(1) + assert ok.verified, ok.reasons + assert recheck(contract, ok, params=params, actor=bot).outcome == "CONFIRMED" + + params, extra = save(2, "INSERT INTO note (body) VALUES ('stray')") + assert not extra.verified and extra.outcome == "REFUTED", extra.reasons + + # Review #1673, B1: a stray row in a schema the contract doesn't name. + params, other = save(4, "INSERT INTO other.note (body) VALUES ('stray')") + assert not other.verified and other.outcome == "REFUTED", other.reasons + + params, late = save(3) + assert late.verified, late.reasons + pg.sql("UPDATE contact SET name = 'Eve' WHERE email = 'ada3@example.test'") + assert recheck(contract, late, params=params, actor=bot).outcome == "REFUTED" From bd2397331e3400e26dab3ac315b95ed38103dcbf Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 12:55:33 -0400 Subject: [PATCH 39/94] WIP: verify-contract generator from a SQL schema and demonstrations --- .../openadapt_flow/generate/demo_analysis.py | 625 ++++++ .../flow/openadapt_flow/generate/sql_store.py | 518 +++++ .../generate/verify_contract.py | 1912 +++++++++++++++++ .../tests/test_generate_verify_contract.py | 335 +++ 4 files changed, 3390 insertions(+) create mode 100644 packages/flow/openadapt_flow/generate/demo_analysis.py create mode 100644 packages/flow/openadapt_flow/generate/sql_store.py create mode 100644 packages/flow/openadapt_flow/generate/verify_contract.py create mode 100644 packages/flow/tests/test_generate_verify_contract.py diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py new file mode 100644 index 0000000000..404033242f --- /dev/null +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -0,0 +1,625 @@ +"""Find where each parameter of a kind of write lands, from demonstration diffs. + +A demonstration is one correct write of a kind, made by the person or the +setup agent, with the store snapshotted before and after +(:mod:`openadapt_flow.generate.sql_store`). This module reads the diffs and +says, for each parameter, which table and column holds its value, and by +which exact rule: + +- ``exact``: the stored text equals the value's text; +- ``email``: equal after case folding (``ValueExpr`` kind ``email``); +- ``phone``: equal digits (``ValueExpr`` kind ``phone``); +- ``number``, ``date``, ``datetime``: equal by value, compared in SQL after a + cast of the parameter to the column's type; +- ``minutes``: the number of minutes between two date-time columns of the + row; +- a display column of a row another column points to (a declared foreign + key, or a column whose value is that row's primary key), or two text + columns joined by one space (a person's first and last name); +- a row the write added beside the main record, linked to it by a column + that holds the main record's key. + +Every rule is exact: no fuzzy matching, no model. A parameter found in more +than one place in every demonstration, or in none, is reported, and the +generator turns it into a question for a person. +""" + +from __future__ import annotations + +import datetime as _dt +import decimal +import re +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from typing import Any, Optional + +from openadapt_flow.generate.sql_store import ( + Diff, + RowChange, + Schema, + Table, + TableRows, +) +from openadapt_flow.runtime.effects.effect import kind_form + +#: Rules a value can be found by, most exact first. +RULES = ("exact", "email", "phone", "number", "date", "datetime", "minutes") + +_IDLIKE = re.compile(r"(^id$|_id$|^id_|^fk_|_fk$|^parent$|^parent_id$)", re.I) +_IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +#: Table names an app's own logs usually carry. Only a proposal: a person +#: confirms which tables are logs (the bookkeeping question). +_LOG_NAME = re.compile( + r"(log|history|audit|session|token|notification|queue|job|stat|cache|lock|event)", + re.I, +) + + +def value_text(value: Any) -> str: + """A parameter value as text, the way the verifier renders it.""" + + from openadapt_flow.runtime.authorization import runtime_param_text + + if isinstance(value, str): + return value + return runtime_param_text(value) + + +def _decimal(text: Any) -> Optional[decimal.Decimal]: + if text is None or isinstance(text, bool): + return None + try: + d = decimal.Decimal(str(text).strip()) + except (decimal.InvalidOperation, ValueError): + return None + return d if d.is_finite() else None + + +def _datetime(text: str) -> Optional[_dt.datetime]: + for fmt in ( + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%dT%H:%M", + "%Y-%m-%dT%H:%M:%SZ", + "%Y-%m-%dT%H:%MZ", + ): + try: + return _dt.datetime.strptime(text.strip(), fmt) + except ValueError: + continue + return None + + +def rule_for(stored: Optional[str], column_kind: str, value: Any) -> Optional[str]: + """The most exact rule by which ``stored`` holds ``value``, or None.""" + + if stored is None or value is None or isinstance(value, (list, dict)): + return None + text = value_text(value) + if text == "": + return None + if stored == text: + return "exact" + if column_kind == "text": + if "@" in text and stored.casefold() == text.casefold(): + return "email" + digits = sum(ch.isdigit() for ch in text) + if digits >= 6: + a, b = kind_form("phone", stored), kind_form("phone", text) + if a is not None and a == b: + return "phone" + return None + if column_kind in ("number", "bool"): + a, b = _decimal(stored), _decimal(text) + if a is not None and b is not None and a == b: + return "number" + return None + if column_kind == "date": + return "date" if stored[:10] == text and len(text) == 10 else None + if column_kind == "datetime": + a, b = _datetime(stored.split(".")[0]), _datetime(text) + if a is not None and b is not None and a == b: + return "datetime" + return None + return None + + +@dataclass(frozen=True) +class Hop: + """One step from a row to another row. + + ``fk``: this row's ``column`` holds the key ``ref_column`` of a row of + ``table``. ``child``: a row of ``table`` whose ``column`` holds this row's + primary key ``ref_column``. + """ + + kind: str # fk | child + table: str + column: str + ref_column: str + declared: bool = False + + def to_json(self) -> dict[str, Any]: + return { + "kind": self.kind, + "table": self.table, + "column": self.column, + "ref_column": self.ref_column, + "declared": self.declared, + } + + @classmethod + def from_json(cls, d: Mapping[str, Any]) -> "Hop": + return cls( + str(d["kind"]), + str(d["table"]), + str(d["column"]), + str(d["ref_column"]), + bool(d.get("declared", False)), + ) + + +@dataclass(frozen=True) +class Location: + """Where a value is: a path of hops from the main row, then the column + (or two columns joined by a space) that holds it, and the rule.""" + + hops: tuple[Hop, ...] + columns: tuple[str, ...] + rule: str + + @property + def table_hint(self) -> str: + return self.hops[-1].table if self.hops else "" + + def signature(self) -> str: + path = "/".join( + f"{h.kind}:{h.table}.{h.column}={h.ref_column}" for h in self.hops + ) + return f"{path}#{'+'.join(self.columns)}@{self.rule}" + + def describe(self, anchor: str) -> str: + where = anchor + for h in self.hops: + if h.kind == "fk": + where = f"{where}.{h.column} -> {h.table}" + else: + where = f"{h.table} (whose {h.column} is the {where} key)" + cols = " || ' ' || ".join(self.columns) + return f"{where}.{cols} ({self.rule})" + + def to_json(self) -> dict[str, Any]: + return { + "hops": [h.to_json() for h in self.hops], + "columns": list(self.columns), + "rule": self.rule, + } + + @classmethod + def from_json(cls, d: Mapping[str, Any]) -> "Location": + return cls( + tuple(Hop.from_json(h) for h in d.get("hops", [])), + tuple(d["columns"]), + str(d["rule"]), + ) + + +def _hop_cost(h: Hop) -> int: + """0 for a declared foreign key, 1 for a column named after the table it + points to (``status_id`` -> ``issue_statuses``), 2 for a value match only.""" + + if h.declared: + return 0 + stem = re.sub(r"(^fk_|_id$|_fk$|^id_)", "", h.column.lower()).rstrip("s") + table = h.table.lower() + return 1 if stem and len(stem) >= 3 and stem in table else 2 + + +def _rank(loc: Location) -> tuple[int, int, int, int]: + return ( + len(loc.hops), + sum(_hop_cost(h) for h in loc.hops), + RULES.index(loc.rule) if loc.rule in RULES else 99, + len(loc.columns), + ) + + +@dataclass +class Demo: + """One demonstration: the kind, the values given, and the diff.""" + + kind: str + values: dict[str, Any] + diff: Diff + after: Mapping[str, TableRows] + started_at: str = "" + finished_at: str = "" + name: str = "" + + +class _Index: + """Rows by table and single-column key, from one snapshot.""" + + def __init__(self, schema: Schema, rows: Mapping[str, TableRows]) -> None: + self.schema = schema + self.rows = rows + self._by_key: dict[str, dict[str, dict[str, Optional[str]]]] = {} + for name, table in schema.tables.items(): + t = rows.get(name) + if t is None or t.count_only is not None or len(table.primary_key) != 1: + continue + pk = table.primary_key[0] + self._by_key[name] = { + str(r.get(pk)): r for r in t.rows.values() if r.get(pk) is not None + } + + def row(self, table: str, key: str) -> Optional[dict[str, Optional[str]]]: + return self._by_key.get(table, {}).get(key) + + def keyed_tables(self) -> list[str]: + return list(self._by_key) + + +def _fk_targets( + schema: Schema, idx: _Index, table: Table, column: str, value: str +) -> list[tuple[str, str, bool]]: + """Rows ``table.column = value`` may point to: (table, key column, + declared).""" + + out = [] + for fk in table.foreign_keys: + if fk.column == column and idx.row(fk.table, value) is not None: + target = schema.tables.get(fk.table) + if target is not None and target.primary_key == [fk.ref_column]: + out.append((fk.table, fk.ref_column, True)) + if out: + return out + if not _IDLIKE.search(column) or value in ("", "0"): + return [] + for name in idx.keyed_tables(): + if idx.row(name, value) is not None: + out.append((name, schema.tables[name].primary_key[0], False)) + return out + + +def _display_matches( + table: Table, row: Mapping[str, Optional[str]], value: Any +) -> list[tuple[tuple[str, ...], str]]: + """Columns of ``row`` that hold ``value``, with the rule.""" + + out: list[tuple[tuple[str, ...], str]] = [] + for col in table.columns: + rule = rule_for(row.get(col.name), col.kind, value) + if rule is not None: + out.append(((col.name,), rule)) + if isinstance(value, str) and " " in value.strip(): + texts = [c for c in table.columns if c.kind == "text"] + for i, a in enumerate(texts): + for b in texts[i + 1 : i + 3]: + va, vb = row.get(a.name), row.get(b.name) + if va and vb and f"{va} {vb}" == value: + out.append(((a.name, b.name), "exact")) + if isinstance(value, (int, float)) and not isinstance(value, bool): + stamps = [c for c in table.columns if c.kind == "datetime"] + for a in stamps: + for b in stamps: + if a is b: + continue + ta, tb = row.get(a.name), row.get(b.name) + da = _datetime((ta or "").split(".")[0]) if ta else None + db = _datetime((tb or "").split(".")[0]) if tb else None + if da and db and db > da: + minutes = decimal.Decimal((db - da).total_seconds()) / 60 + if minutes == _decimal(value_text(value)): + out.append(((a.name, b.name), "minutes")) + return out + + +def locate( + schema: Schema, + demo: Demo, + anchor: RowChange, + value: Any, + *, + before: bool = False, + changed_only: bool = False, + max_hops: int = 2, + links: Optional[Mapping[str, set[str]]] = None, +) -> list[Location]: + """Every location of ``value`` from the main row ``anchor``. + + ``before``: look in the row as it was before the write (an update's + identity). ``changed_only``: at depth 0, only columns the write changed + (an update's new values). + """ + + idx = _Index(schema, demo.after) + table = schema.tables[anchor.table] + row = anchor.before if before else anchor.after + if row is None: + return [] + found: list[Location] = [] + changed = set(anchor.changed_columns) + added = set() + for ch in demo.diff.changes: + t2 = schema.tables.get(ch.table) + if ch.kind == "added" and ch.after and t2 is not None and len(t2.primary_key) == 1: + added.add((ch.table, ch.after.get(t2.primary_key[0]))) + + def walk( + t: Table, r: Mapping[str, Optional[str]], hops: tuple[Hop, ...], depth: int + ) -> None: + for cols, rule in _display_matches(t, r, value): + if depth == 0 and changed_only and not (set(cols) & changed): + continue + found.append(Location(hops, cols, rule)) + if depth >= max_hops: + return + for col in t.columns: + v = r.get(col.name) + if v is None or (col.name in t.primary_key and hops): + continue + if depth == 0 and changed_only and col.name not in changed: + continue + for target, ref, declared in _fk_targets(schema, idx, t, col.name, v): + target_row = idx.row(target, v) + if target_row is None or (target == anchor.table and hops): + continue # back to the main record: not a location + if not declared and not hops and (target, v) in added: + # The main row's key columns point at rows that were + # there before; a row the write added is reached from a + # row that names the main record instead. + continue + walk( + schema.tables[target], + target_row, + hops + (Hop("fk", target, col.name, ref, declared),), + depth + 1, + ) + if depth == 0 and len(t.primary_key) == 1 and not before: + key = r.get(t.primary_key[0]) + if key is None: + return + for ch in demo.diff.changes: + if ch.kind != "added" or ch.table == t.name or ch.after is None: + continue + child = schema.tables.get(ch.table) + if child is None: + continue + for col in child.columns: + if ch.after.get(col.name) != key: + continue + if links is not None and col.name not in links.get(ch.table, {col.name}): + continue + declared = any( + fk.column == col.name and fk.table == t.name + for fk in child.foreign_keys + ) + if not declared and not _IDLIKE.search(col.name): + continue + walk( + child, + ch.after, + hops + + (Hop("child", ch.table, col.name, t.primary_key[0], declared),), + depth + 1, + ) + + walk(table, row, (), 0) + unique: dict[str, Location] = {} + for loc in found: + unique.setdefault(loc.signature(), loc) + return sorted(unique.values(), key=_rank) + + +def link_columns(schema: Schema, demos: Sequence[tuple["Demo", RowChange]]) -> dict[str, set[str]]: + """Per table, the columns that hold the main record's key in every row + that table gained, across all demonstrations (a journal's + ``journalized_id``, not its ``user_id`` that happens to be equal once).""" + + out: dict[str, set[str]] = {} + for demo, anchor in demos: + t = schema.tables.get(anchor.table) + row = anchor.after or {} + if t is None or len(t.primary_key) != 1 or row.get(t.primary_key[0]) is None: + continue + key = row[t.primary_key[0]] + for ch in demo.diff.changes: + if ch.kind != "added" or ch.table == anchor.table or ch.after is None: + continue + cols = {c for c, v in ch.after.items() if v == key} + out[ch.table] = out[ch.table] & cols if ch.table in out else cols + return out + + +def best(locations: Sequence[Location]) -> tuple[Optional[Location], list[Location]]: + """The single best location, or None and the tied ones.""" + + if not locations: + return None, [] + ranked = sorted(locations, key=_rank) + top = [loc for loc in ranked if _rank(loc) == _rank(ranked[0])] + if len(top) == 1: + return top[0], [] + return None, top + + +def pick_anchor( + schema: Schema, demo: Demo, params: Sequence[str] +) -> tuple[Optional[RowChange], str, int]: + """The main row of a demonstration: the added row (a create) or changed + row (an update) that holds the most parameter values. ``mode`` is + ``create`` or ``update``; ``score`` how many values it holds.""" + + best_row: Optional[RowChange] = None + best_mode, best_score, best_direct = "", 0, 0 + for ch in demo.diff.changes: + if ch.kind == "removed" or ch.table not in schema.tables: + continue + mode = "create" if ch.kind == "added" else "update" + score = direct = 0 + for p in params: + v = demo.values.get(p) + if v is None or isinstance(v, (list, dict)): + continue + if mode == "update": + hit = locate(schema, demo, ch, v, before=True, max_hops=1) or locate( + schema, demo, ch, v, changed_only=True, max_hops=1 + ) + else: + hit = locate(schema, demo, ch, v, max_hops=1) + score += 1 if hit else 0 + direct += 1 if any(not loc.hops for loc in hit) else 0 + if (score, direct) > (best_score, best_direct): + best_row, best_mode, best_score, best_direct = ch, mode, score, direct + return best_row, best_mode, best_score + + +def is_identifier(value: Any) -> bool: + return isinstance(value, str) and bool(_IDENT.match(value)) + + +def snake(name: str) -> str: + return re.sub(r"(? bool: + return bool(_LOG_NAME.search(table)) + + +@dataclass +class ColumnRole: + """What a column of a new row is, when no parameter explains it.""" + + role: str # key | timestamp | actor | link | copied | constant | varies | default + value: Optional[str] = None + detail: dict[str, Any] = field(default_factory=dict) + + +def _within(stamp: Optional[str], start: str, end: str) -> bool: + if not stamp or not start or not end: + return False + when = _datetime(stamp.split(".")[0].split("+")[0]) + if when is None: + try: + when = _dt.datetime.strptime(stamp[:10], "%Y-%m-%d") + except ValueError: + return False + lo = _dt.datetime.fromisoformat(start.replace("Z", "")) - _dt.timedelta(days=1) + hi = _dt.datetime.fromisoformat(end.replace("Z", "")) + _dt.timedelta(days=1) + return lo <= when <= hi + + +def column_roles( + schema: Schema, + table: str, + rows: Sequence[tuple[Demo, Mapping[str, Optional[str]]]], + explained: set[str], + actor_values: Mapping[str, str], + link_values: Sequence[set[str]] = (), +) -> dict[str, ColumnRole]: + """Roles of the columns of new rows of ``table`` that no parameter + explains, across every demonstration row given (one row per demo or + several). ``link_values``: per row, keys of rows the same write added + (a column holding one is a link).""" + + t = schema.tables[table] + out: dict[str, ColumnRole] = {} + for col in t.columns: + name = col.name + if name in explained: + continue + values = [r.get(name) for _, r in rows] + if col.auto or (name in t.primary_key and len(t.primary_key) == 1): + out[name] = ColumnRole("key") + continue + if col.kind in ("date", "datetime") and all( + v is None or _within(v, d.started_at, d.finished_at) + for (d, _), v in zip(rows, values) + ) and any(v is not None for v in values): + out[name] = ColumnRole("timestamp") + continue + actor = [ + p + for p, av in actor_values.items() + if values and all(v is not None and v == av for v in values) + ] + if actor: + out[name] = ColumnRole("actor", detail={"param": actor[0]}) + continue + links = link_values or [set() for _ in rows] + if values and all( + v is not None and (v in lv or (len(t.primary_key) == 1 and v == r.get(t.primary_key[0]))) + for v, lv, (_, r) in zip(values, links, rows) + ): + out[name] = ColumnRole("link") + continue + if len(t.primary_key) == 1 and values and all( + v is not None + and r.get(t.primary_key[0]) is not None + and str(r.get(t.primary_key[0])) in v + and v != r.get(t.primary_key[0]) + for v, (_, r) in zip(values, rows) + ): + out[name] = ColumnRole("varies", detail={"why": "holds the row's own key"}) + continue + copied = _copied(schema, t, name, rows) + if copied is not None: + out[name] = ColumnRole("copied", detail=copied) + continue + distinct = {v for v in values} + if len(distinct) == 1: + v = values[0] + default = col.default + if v is None: + out[name] = ColumnRole("constant", None) + else: + out[name] = ColumnRole( + "constant", v, detail={"is_default": default is not None and default.strip("'") == v} + ) + continue + out[name] = ColumnRole("varies") + return out + + +def _copied( + schema: Schema, + t: Table, + name: str, + rows: Sequence[tuple[Demo, Mapping[str, Optional[str]]]], +) -> Optional[dict[str, Any]]: + """Is the column a copy of one column of the row an FK points to, in + every row? (An invoice line's unit price is its product's price.)""" + + col = t.column(name) + if col is None or col.kind not in ("number", "text"): + return None + candidates: Optional[set[tuple[str, str, str, str]]] = None + for demo, r in rows: + v = r.get(name) + if v is None: + return None + idx = _Index(schema, demo.after) + here: set[tuple[str, str, str, str]] = set() + for other in t.columns: + if other.name == name or r.get(other.name) is None: + continue + for target, ref, _declared in _fk_targets( + schema, idx, t, other.name, str(r.get(other.name)) + ): + trow = idx.row(target, str(r.get(other.name))) + if trow is None: + continue + for tc in schema.tables[target].columns: + if tc.name == ref: + continue + if rule_for(trow.get(tc.name), tc.kind, v) in ("exact", "number"): + here.add((other.name, target, ref, tc.name)) + candidates = here if candidates is None else candidates & here + if not candidates: + return None + if candidates and len(candidates) == 1: + via, target, ref, source = next(iter(candidates)) + return {"via": via, "table": target, "ref_column": ref, "column": source} + return None diff --git a/packages/flow/openadapt_flow/generate/sql_store.py b/packages/flow/openadapt_flow/generate/sql_store.py new file mode 100644 index 0000000000..df0a7d666c --- /dev/null +++ b/packages/flow/openadapt_flow/generate/sql_store.py @@ -0,0 +1,518 @@ +"""Read a SQL database's schema and rows for the contract generator. + +The generator (:mod:`openadapt_flow.generate.verify_contract`) needs two +things from the store a write lands in: + +- the **schema**, from ``information_schema`` (MySQL, MariaDB, PostgreSQL): + every base table, its columns with their types, defaults and nullability, + the primary key, declared foreign keys, and the columns the database fills + on its own (auto-increment keys, ``ON UPDATE CURRENT_TIMESTAMP``); +- **row snapshots** before and after a demonstration write, so a diff shows + which tables, rows and columns one kind of write touches. + +Everything is read through the read-only connection the contract will use +(``sql_driver``, ``sql_connect_args``, ``sql_password_env``), with the +driver's read-only session switched on. Nothing here writes, and nothing +calls a model. + +A snapshot holds the rows' values. It is written only into the generator's +own working directory, which the person keeps private, like any database +export. +""" + +from __future__ import annotations + +import datetime as _dt +import decimal +import importlib +import json +import os +from collections.abc import Callable, Mapping, Sequence +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Any, Optional + +#: Dialects this module reads. +DIALECTS = ("mysql", "postgresql") + +#: Above this many rows a table is snapshotted by count and largest key only; +#: a demonstration diff can't say which of its rows changed. +DEFAULT_MAX_ROWS = 20000 + + +class StoreError(RuntimeError): + """The store can't be read as the generator needs.""" + + +@dataclass +class Column: + name: str + data_type: str + nullable: bool + default: Optional[str] + #: The database fills it on insert (auto-increment, a sequence, identity). + auto: bool = False + #: The database rewrites it on every update (``ON UPDATE CURRENT_TIMESTAMP``). + on_update: bool = False + + @property + def kind(self) -> str: + """``text``, ``number``, ``date``, ``datetime``, ``time``, ``bool``, + ``binary`` or ``other``.""" + + t = self.data_type.lower() + if t in ("tinyint(1)", "boolean", "bool"): + return "bool" + if any( + t.startswith(p) + for p in ( + "int", + "tinyint", + "smallint", + "mediumint", + "bigint", + "integer", + "decimal", + "numeric", + "float", + "double", + "real", + ) + ): + return "number" + if t == "date": + return "date" + if t.startswith(("datetime", "timestamp")): + return "datetime" + if t.startswith("time"): + return "time" + if any( + t.startswith(p) + for p in ("char", "varchar", "text", "tinytext", "mediumtext", "longtext") + ) or t in ("character varying", "character", "citext", "enum", "set"): + return "text" + if t.startswith(("character varying", "character")): + return "text" + if any(t.startswith(p) for p in ("blob", "binary", "varbinary", "bytea")): + return "binary" + return "other" + + +@dataclass +class ForeignKey: + column: str + table: str + ref_column: str + + +@dataclass +class Table: + name: str + columns: list[Column] + primary_key: list[str] + foreign_keys: list[ForeignKey] = field(default_factory=list) + + def column(self, name: str) -> Optional[Column]: + for c in self.columns: + if c.name == name: + return c + return None + + @property + def column_names(self) -> list[str]: + return [c.name for c in self.columns] + + +@dataclass +class Schema: + dialect: str + database: str + tables: dict[str, Table] + #: Views the read account sees, by name (they are read, never written). + views: list[str] = field(default_factory=list) + + def to_json(self) -> dict[str, Any]: + return { + "dialect": self.dialect, + "database": self.database, + "views": list(self.views), + "tables": {n: asdict(t) for n, t in self.tables.items()}, + } + + @classmethod + def from_json(cls, data: Mapping[str, Any]) -> "Schema": + tables = {} + for name, t in dict(data["tables"]).items(): + tables[name] = Table( + name=t["name"], + columns=[Column(**c) for c in t["columns"]], + primary_key=list(t["primary_key"]), + foreign_keys=[ForeignKey(**f) for f in t.get("foreign_keys", [])], + ) + return cls( + str(data["dialect"]), + str(data["database"]), + tables, + list(data.get("views", [])), + ) + + +def dialect_of(read: Mapping[str, Any]) -> str: + """``mysql`` or ``postgresql``, from the read's driver.""" + + driver = str(read.get("sql_driver") or "") + if driver in ("pymysql", "MySQLdb", "mysql.connector"): + return "mysql" + if driver in ("psycopg2", "psycopg"): + return "postgresql" + raise StoreError( + f"sql_driver {driver!r}: the generator reads MySQL/MariaDB (pymysql) " + "or PostgreSQL (psycopg2)" + ) + + +def connector(read: Mapping[str, Any]) -> Callable[[], Any]: + """A function that opens one read-only connection, as the contract will.""" + + driver = str(read.get("sql_driver") or "") + dialect_of(read) + module = importlib.import_module(driver) + args = dict(read.get("sql_connect_args") or {}) + env = read.get("sql_password_env") + if env: + if env not in os.environ: + raise StoreError(f"environment variable {env} (the read password) is not set") + args["password"] = os.environ[str(env)] + + def connect() -> Any: + from openadapt_flow.runtime.effects.sql import read_only_connection + + conn = module.connect(**args) + return read_only_connection(conn, driver) + + return connect + + +def _rows(cur: Any, sql: str, params: Sequence[Any] = ()) -> list[tuple[Any, ...]]: + cur.execute(sql, tuple(params)) if params else cur.execute(sql) + return [tuple(r) for r in cur.fetchall()] + + +def read_schema(connect: Callable[[], Any], dialect: str) -> Schema: + """Every base table the read account sees, with columns, keys and FKs.""" + + conn = connect() + try: + cur = conn.cursor() + if dialect == "mysql": + return _mysql_schema(cur) + if dialect == "postgresql": + return _pg_schema(cur) + raise StoreError(f"dialect {dialect!r} is not one of {DIALECTS}") + finally: + try: + conn.rollback() + finally: + conn.close() + + +def _mysql_schema(cur: Any) -> Schema: + (db,) = _rows(cur, "SELECT DATABASE()")[0] + if not db: + raise StoreError("the connection names no database (sql_connect_args.database)") + kinds = dict( + _rows( + cur, + "SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES " + "WHERE TABLE_SCHEMA = %s", + (db,), + ) + ) + cols = _rows( + cur, + "SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, DATA_TYPE, IS_NULLABLE, " + "COLUMN_DEFAULT, EXTRA, COLUMN_KEY FROM information_schema.COLUMNS " + "WHERE TABLE_SCHEMA = %s ORDER BY TABLE_NAME, ORDINAL_POSITION", + (db,), + ) + keys = _rows( + cur, + "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, " + "REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE " + "WHERE TABLE_SCHEMA = %s ORDER BY TABLE_NAME, CONSTRAINT_NAME, " + "ORDINAL_POSITION", + (db,), + ) + tables: dict[str, Table] = {} + for tname, cname, ctype, dtype, nullable, default, extra, _key in cols: + if kinds.get(tname) != "BASE TABLE": + continue + extra = str(extra or "").lower() + ctype = str(ctype).lower() + t = tables.setdefault(tname, Table(tname, [], [])) + t.columns.append( + Column( + name=str(cname), + data_type="tinyint(1)" if ctype.startswith("tinyint(1)") else str(dtype).lower(), + nullable=str(nullable).upper() == "YES", + default=None if default is None else str(default), + auto="auto_increment" in extra, + on_update="on update" in extra, + ) + ) + for tname, cname, cons, rtable, rcol in keys: + t = tables.get(tname) + if t is None: + continue + if cons == "PRIMARY": + t.primary_key.append(str(cname)) + elif rtable: + t.foreign_keys.append(ForeignKey(str(cname), str(rtable), str(rcol))) + views = sorted(n for n, k in kinds.items() if k == "VIEW") + return Schema("mysql", str(db), tables, views) + + +def _pg_schema(cur: Any) -> Schema: + (schema,) = _rows(cur, "SELECT current_schema()")[0] + (db,) = _rows(cur, "SELECT current_database()")[0] + kinds = dict( + _rows( + cur, + "SELECT table_name, table_type FROM information_schema.tables " + "WHERE table_schema = %s", + (schema,), + ) + ) + cols = _rows( + cur, + "SELECT table_name, column_name, data_type, is_nullable, column_default, " + "is_identity FROM information_schema.columns WHERE table_schema = %s " + "ORDER BY table_name, ordinal_position", + (schema,), + ) + tables: dict[str, Table] = {} + for tname, cname, dtype, nullable, default, identity in cols: + if kinds.get(tname) != "BASE TABLE": + continue + t = tables.setdefault(tname, Table(tname, [], [])) + default_s = None if default is None else str(default) + t.columns.append( + Column( + name=str(cname), + data_type=str(dtype).lower(), + nullable=str(nullable).upper() == "YES", + default=default_s, + auto=(default_s or "").startswith("nextval(") + or str(identity).upper() == "YES", + ) + ) + # pg_catalog: information_schema hides constraints of tables the account + # doesn't own, and the read account owns none. + cons = _rows( + cur, + "SELECT c.conrelid::regclass::text, c.contype, a.attname, " + "c.confrelid::regclass::text, fa.attname " + "FROM pg_constraint c " + "JOIN pg_namespace n ON n.oid = c.connamespace " + "CROSS JOIN LATERAL unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord) " + "JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum " + "LEFT JOIN pg_attribute fa ON fa.attrelid = c.confrelid " + "AND fa.attnum = c.confkey[k.ord] " + "WHERE n.nspname = %s AND c.contype IN ('p', 'f') " + "ORDER BY 1, c.conname, k.ord", + (schema,), + ) + for tname, ctype, cname, rtable, rcol in cons: + t = tables.get(str(tname).split(".")[-1].strip('"')) + if t is None: + continue + if ctype == "p": + t.primary_key.append(str(cname)) + elif rtable and rcol: + t.foreign_keys.append( + ForeignKey(str(cname), str(rtable).split(".")[-1].strip('"'), str(rcol)) + ) + views = sorted(n for n, k in kinds.items() if k == "VIEW") + return Schema("postgresql", str(db), tables, views) + + +def quote(dialect: str, name: str) -> str: + """A quoted identifier. Names with a quote character are refused.""" + + if "`" in name or '"' in name or "\x00" in name: + raise StoreError(f"identifier {name!r} holds a quote character") + return f"`{name}`" if dialect == "mysql" else f'"{name}"' + + +def cell_text(value: Any) -> Optional[str]: + """One cell as text, the way the generator compares values: ``None`` + stays ``None``; bytes are ``0x`` + hex; dates are ISO; a Decimal keeps its + stored digits.""" + + if value is None: + return None + if isinstance(value, (bytes, bytearray, memoryview)): + return "0x" + bytes(value).hex() + if isinstance(value, _dt.datetime): + return value.isoformat(sep=" ") + if isinstance(value, (_dt.date, _dt.time)): + return value.isoformat() + if isinstance(value, decimal.Decimal): + return format(value, "f") + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, float) and value.is_integer(): + return str(int(value)) + if isinstance(value, (dict, list)): + return json.dumps(value, sort_keys=True) + return str(value) + + +@dataclass +class TableRows: + #: Row key (the primary key's values joined by ``|``, or the whole row + #: for a table without one) -> column -> text. + rows: dict[str, dict[str, Optional[str]]] + #: Set when the table had more rows than the snapshot keeps. + count_only: Optional[int] = None + + +def row_key(table: Table, row: Mapping[str, Optional[str]]) -> str: + cols = table.primary_key or table.column_names + return "|".join("\x00" if row.get(c) is None else str(row.get(c)) for c in cols) + + +def snapshot( + connect: Callable[[], Any], + schema: Schema, + *, + max_rows: int = DEFAULT_MAX_ROWS, +) -> dict[str, TableRows]: + """Every table's rows as text, in one read-only transaction.""" + + conn = connect() + out: dict[str, TableRows] = {} + try: + cur = conn.cursor() + if schema.dialect == "mysql": + cur.execute("START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY") + else: + cur.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ") + for name in sorted(schema.tables): + table = schema.tables[name] + q = quote(schema.dialect, name) + (count,) = _rows(cur, f"SELECT COUNT(*) FROM {q}")[0] + if int(count) > max_rows: + out[name] = TableRows({}, count_only=int(count)) + continue + cols = table.column_names + cur.execute( + "SELECT " + + ", ".join(quote(schema.dialect, c) for c in cols) + + f" FROM {q}" + ) + rows: dict[str, dict[str, Optional[str]]] = {} + for raw in cur.fetchall(): + row = {c: cell_text(v) for c, v in zip(cols, raw)} + key = row_key(table, row) + while key in rows: # identical rows in a table without a key + key += "+" + rows[key] = row + out[name] = TableRows(rows) + finally: + try: + conn.rollback() + finally: + conn.close() + return out + + +def save_snapshot(path: Path, rows: Mapping[str, TableRows]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + data = {n: {"rows": t.rows, "count_only": t.count_only} for n, t in rows.items()} + tmp = path.with_suffix(".tmp") + tmp.write_text(json.dumps(data, sort_keys=True), encoding="utf-8") + os.chmod(tmp, 0o600) + tmp.replace(path) + + +def load_snapshot(path: Path) -> dict[str, TableRows]: + data = json.loads(path.read_text(encoding="utf-8")) + return { + n: TableRows(dict(t["rows"]), t.get("count_only")) for n, t in data.items() + } + + +@dataclass +class RowChange: + table: str + key: str + kind: str # added | removed | changed + before: Optional[dict[str, Optional[str]]] + after: Optional[dict[str, Optional[str]]] + + @property + def changed_columns(self) -> list[str]: + if self.before is None or self.after is None: + return [] + return sorted( + c + for c in set(self.before) | set(self.after) + if self.before.get(c) != self.after.get(c) + ) + + def to_json(self) -> dict[str, Any]: + return asdict(self) + + +@dataclass +class Diff: + changes: list[RowChange] + #: Tables too large to diff row by row whose count changed. + unread: dict[str, tuple[int, int]] = field(default_factory=dict) + + def tables(self) -> list[str]: + return sorted({c.table for c in self.changes} | set(self.unread)) + + def of(self, table: str, kind: Optional[str] = None) -> list[RowChange]: + return [ + c for c in self.changes if c.table == table and (kind is None or c.kind == kind) + ] + + def to_json(self) -> dict[str, Any]: + return { + "changes": [c.to_json() for c in self.changes], + "unread": {k: list(v) for k, v in self.unread.items()}, + } + + @classmethod + def from_json(cls, data: Mapping[str, Any]) -> "Diff": + return cls( + [RowChange(**c) for c in data.get("changes", [])], + {k: (int(v[0]), int(v[1])) for k, v in dict(data.get("unread", {})).items()}, + ) + + +def diff(before: Mapping[str, TableRows], after: Mapping[str, TableRows]) -> Diff: + """Rows added, removed or changed, table by table.""" + + changes: list[RowChange] = [] + unread: dict[str, tuple[int, int]] = {} + for name in sorted(set(before) | set(after)): + a, b = before.get(name), after.get(name) + if a is None or b is None: + unread[name] = (0 if a is None else len(a.rows), 0 if b is None else len(b.rows)) + continue + if a.count_only is not None or b.count_only is not None: + ca = a.count_only if a.count_only is not None else len(a.rows) + cb = b.count_only if b.count_only is not None else len(b.rows) + if ca != cb: + unread[name] = (ca, cb) + continue + for key in sorted(set(a.rows) | set(b.rows)): + ra, rb = a.rows.get(key), b.rows.get(key) + if ra == rb: + continue + kind = "added" if ra is None else "removed" if rb is None else "changed" + changes.append(RowChange(name, key, kind, ra, rb)) + return Diff(changes, unread) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py new file mode 100644 index 0000000000..c897bf84ca --- /dev/null +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -0,0 +1,1912 @@ +"""Propose ``openadapt.verify/v1`` contracts from a SQL schema and demonstrations. + + python -m openadapt_flow.generate.verify_contract --help + +A person setting up the verifier for an app used to write every contract by +hand, with 10 or 11 decisions per app. This generator writes them from: + +- the store's schema (``information_schema`` on MySQL, MariaDB or + PostgreSQL), read through the read-only account the contracts will use; +- a short description of each kind of write: its parameter names and one or + two example tasks (the ``kinds.yaml`` a setup agent is given); +- one demonstration write per example, which it snapshots before and after + to see which tables and columns the kind touches. + +For each kind it proposes one contract per field set the kind's tasks can +use (an update that may change the phone, the email or both gets a contract +for each), reading the whole database so an extra record anywhere is seen, +and a manifest that picks the contract for a task's values +(:func:`choose`). Whatever it can't decide alone becomes one numbered +question in ``QUESTIONS.md``; the goal is at most three per app. Answers go +in ``answers.yaml`` and the generator is run again. + +It never emits a contract that reads something that doesn't exist: every +contract is loaded and read once against the live store (a dry read) before +it is written out, and a contract that needs a database view gets it created +(``--create-views``, through an admin connection) or is refused with the +reason. See ``docs/GENERATE_CONTRACTS.md``. Nothing here calls a model. +""" + +from __future__ import annotations + +import argparse +import datetime as _dt +import hashlib +import itertools +import json +import os +import re +import sys +import tempfile +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Optional + +import yaml + +from openadapt_flow.generate import sql_store as store +from openadapt_flow.generate.demo_analysis import ( + _rank, + ColumnRole, + Demo, + Hop, + Location, + best, + column_roles, + is_identifier, + link_columns, + locate, + looks_like_log, + pick_anchor, + snake, +) +from openadapt_flow.generate.sql_store import RowChange, Schema, Table + +MANIFEST_SCHEMA = "openadapt.contract-manifest/v1" +#: Most contracts one kind gets (all subsets of up to six optional fields). +MAX_VARIANTS = 64 +#: Longest list a list parameter gets a contract for. +MAX_LIST = 10 +#: The recheck window proposed when the person hasn't answered. +DEFAULT_RECHECK_S = 300 + + +class GenerateError(RuntimeError): + """The generator can't go on; the message says why.""" + + +# -- inputs ------------------------------------------------------------------- + + +def load_yaml(path: Path) -> Any: + try: + return yaml.safe_load(Path(path).read_text(encoding="utf-8")) + except (OSError, yaml.YAMLError) as exc: + raise GenerateError(f"cannot read {path}: {exc}") from exc + + +def load_read(path: Path) -> dict[str, Any]: + """``read.yaml``: the read-only connection, as in a ``kind: sql`` + verifier (``sql_driver``, ``sql_connect_args``, ``sql_password_env``).""" + + data = load_yaml(path) + if not isinstance(data, dict) or not data.get("sql_driver"): + raise GenerateError(f"{path}: needs sql_driver and sql_connect_args") + if "password" in dict(data.get("sql_connect_args") or {}): + raise GenerateError( + f"{path}: put the password in an environment variable named by " + "sql_password_env, not in the file" + ) + return { + k: data[k] + for k in ("sql_driver", "sql_connect_args", "sql_password_env") + if k in data + } + + +@dataclass +class KindSpec: + kind: str + parameters: list[str] + description: str = "" + examples: list[dict[str, Any]] = field(default_factory=list) + + +def load_kinds(path: Path) -> list[KindSpec]: + data = load_yaml(path) or {} + kinds = data.get("kinds") if isinstance(data, dict) else None + if not isinstance(kinds, list) or not kinds: + raise GenerateError(f"{path}: needs a list under kinds:") + out = [] + for k in kinds: + if not isinstance(k, dict) or not k.get("kind") or not k.get("parameters"): + raise GenerateError(f"{path}: each kind needs kind: and parameters:") + out.append( + KindSpec( + str(k["kind"]), + [str(p) for p in k["parameters"]], + str(k.get("description") or ""), + [ + {"instruction": e.get("instruction", ""), "values": dict(e.get("values") or {})} + for e in k.get("examples") or [] + ], + ) + ) + return out + + +def load_demos(demo_dir: Path) -> list[Demo]: + demos = [] + for f in sorted(Path(demo_dir).glob("*.demo.json")): + d = json.loads(f.read_text(encoding="utf-8")) + after = store.load_snapshot(Path(demo_dir) / d["after_snapshot"]) + demos.append( + Demo( + kind=d["kind"], + values=dict(d["values"]), + diff=store.Diff.from_json(d["diff"]), + after=after, + started_at=d.get("started_at", ""), + finished_at=d.get("finished_at", ""), + name=f.name, + ) + ) + return demos + + +# -- questions ------------------------------------------------------------------ + + +@dataclass +class Question: + key: str + text: str + proposal: Any + options: list[str] = field(default_factory=list) + + +def _answer(answers: Mapping[str, Any], key: str) -> Any: + return (answers or {}).get(key) + + +# -- the plan for one kind -------------------------------------------------------- + + +@dataclass +class ChildGroup: + """Rows a write adds beside the main row that hold a parameter's value.""" + + table: str + link: Hop # child hop from the anchor + params: dict[str, Location] # param -> location relative to the child row + roles: dict[str, ColumnRole] + count: int = 1 + #: List parameter whose items these rows are, if any. + list_param: Optional[str] = None + changed_columns: list[str] = field(default_factory=list) + + +@dataclass +class SideSpec: + table: str + count: int + roles: dict[str, ColumnRole] + #: Used only when this parameter is not in the field set (the table holds + #: that parameter's row otherwise). + unless: Optional[str] = None + #: Added rows of other tables these rows hold the key of (for ownership). + owner: Optional[str] = None + + +@dataclass +class KindPlan: + kind: str + mode: str = "" # create | update + anchor: str = "" + params: list[str] = field(default_factory=list) + optional: list[str] = field(default_factory=list) + identity: list[str] = field(default_factory=list) + changes: list[str] = field(default_factory=list) + locations: dict[str, Location] = field(default_factory=dict) + #: dispatch value -> (value param -> location) + dispatch: Optional[str] = None + dispatch_locations: dict[str, dict[str, Location]] = field(default_factory=dict) + children: dict[str, ChildGroup] = field(default_factory=dict) # by param + lists: dict[str, ChildGroup] = field(default_factory=dict) + added_refs: dict[str, dict[str, Any]] = field(default_factory=dict) + sides: list[SideSpec] = field(default_factory=list) + logs: set[str] = field(default_factory=set) + bookkeeping: dict[str, set[str]] = field(default_factory=dict) + anchor_changed: set[str] = field(default_factory=set) + demos: list[Demo] = field(default_factory=list) + anchors: list[RowChange] = field(default_factory=list) + unresolved: dict[str, list[Location]] = field(default_factory=dict) + refused: list[str] = field(default_factory=list) + removed: dict[str, int] = field(default_factory=dict) + + +def _scalars(values: Mapping[str, Any]) -> dict[str, Any]: + return { + k: v for k, v in values.items() if v is not None and not isinstance(v, (list, dict)) + } + + +def _intersect(per_demo: list[list[Location]]) -> list[Location]: + if not per_demo: + return [] + common = {loc.signature(): loc for loc in per_demo[0]} + for locs in per_demo[1:]: + sigs = {loc.signature() for loc in locs} + common = {s: loc for s, loc in common.items() if s in sigs} + return list(common.values()) + + +def _as_change(table: str, row: Mapping[str, Optional[str]]) -> RowChange: + return RowChange(table, "", "added", None, dict(row)) + + +def analyze( + schema: Schema, + spec: KindSpec, + demos: Sequence[Demo], + actor_values: Mapping[str, str], +) -> KindPlan: + plan = KindPlan(spec.kind, params=list(spec.parameters)) + mine = [d for d in demos if d.kind == spec.kind and d.diff.changes] + if not mine: + plan.refused.append( + "no demonstration write of this kind (record one with " + "`verify_contract record`)" + ) + return plan + examples = [e["values"] for e in spec.examples] + [d.values for d in mine] + plan.optional = sorted( + p for p in spec.parameters if any(v.get(p) is None for v in examples) + ) + for d in mine: + anchor, mode, score = pick_anchor(schema, d, spec.parameters) + if anchor is None or score == 0: + plan.refused.append( + f"{d.name}: no row the write touched holds any of the task's values" + ) + return plan + if plan.anchor and (anchor.table != plan.anchor or mode != plan.mode): + plan.refused.append( + f"{d.name}: the main record is {anchor.table} ({mode}), but other " + f"demonstrations write {plan.anchor} ({plan.mode})" + ) + return plan + plan.anchor, plan.mode = anchor.table, mode + plan.demos.append(d) + plan.anchors.append(anchor) + lists = [p for p in spec.parameters if any(isinstance(d.values.get(p), list) for d in plan.demos)] + links = link_columns(schema, list(zip(plan.demos, plan.anchors))) + scalars = [p for p in spec.parameters if p not in lists] + + # Where each value is, in every demonstration that gives it. + found: dict[str, list[list[Location]]] = {p: [] for p in scalars} + roles_of: dict[str, set[str]] = {p: set() for p in scalars} + for d, a in zip(plan.demos, plan.anchors): + for p, v in _scalars(d.values).items(): + if p not in found: + continue + if plan.mode == "update": + change = locate(schema, d, a, v, changed_only=True, links=links) + ident = [ + loc + for loc in locate(schema, d, a, v, before=True) + if not loc.hops or loc.hops[0].kind == "fk" + ] + if ident and (not change or _rank(ident[0]) < _rank(change[0])): + found[p].append(ident) + roles_of[p].add("identity") + else: + found[p].append(change) + roles_of[p].add("change" if change else "missing") + else: + found[p].append(locate(schema, d, a, v, links=links)) + # A parameter whose value names a field (``field: status``). + for p in scalars: + values = [d.values.get(p) for d in plan.demos if d.values.get(p) is not None] + if ( + len(values) >= 2 + and all(is_identifier(v) for v in values) + and len(set(values)) == len(values) + and all(not locs for locs in found[p]) + and plan.dispatch is None + ): + plan.dispatch = p + for p in scalars: + if p == plan.dispatch: + continue + if plan.dispatch is not None: + by_value: dict[str, list[list[Location]]] = {} + for d, locs in zip( + [d for d in plan.demos if d.values.get(p) is not None], found[p] + ): + by_value.setdefault(str(d.values.get(plan.dispatch)), []).append(locs) + if len(by_value) > 1: + ok = True + for dv, per in by_value.items(): + loc, tied = best(_intersect(per)) + if loc is None: + plan.unresolved[f"{p} (when {plan.dispatch} is {dv})"] = tied + ok = False + else: + plan.dispatch_locations.setdefault(dv, {})[p] = loc + if ok: + plan.changes.append(p) + continue + loc, tied = best(_intersect(found[p])) + if loc is None: + if not found[p]: + continue # never given: no contract uses it + plan.unresolved[p] = tied + continue + plan.locations[p] = loc + if plan.mode == "update": + (plan.identity if "identity" in roles_of[p] else plan.changes).append(p) + # A column one value is already in is not another value's place. + progress = True + while progress and plan.unresolved: + progress = False + claimed = { + (loc.hops[0].column if loc.hops else c) + for loc in plan.locations.values() + for c in loc.columns + } + for name, tied in list(plan.unresolved.items()): + left = [ + loc + for loc in tied + if (loc.hops[0].column if loc.hops else loc.columns[0]) not in claimed + ] + if len(left) == 1 and name in plan.params: + plan.locations[name] = left[0] + plan.unresolved.pop(name) + if plan.mode == "update" and name not in plan.identity: + plan.changes.append(name) + progress = True + if plan.mode == "update" and not plan.identity: + plan.refused.append( + "no parameter names the record before the write, so the update's " + "target can't be told apart from other records" + ) + _children(schema, plan, lists) + _sides(schema, plan, actor_values) + return plan + + +def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: + """Group child-row locations, and find each list parameter's rows.""" + + for p, loc in list(plan.locations.items()): + if not loc.hops or loc.hops[0].kind != "child": + continue + rel = Location(loc.hops[1:], loc.columns, loc.rule) + rows = [] + for d, a in zip(plan.demos, plan.anchors): + if d.values.get(p) is None: + continue + for ch in d.diff.of(loc.hops[0].table, "added"): + if ch.after is None: + continue + key = (a.after or {}).get(loc.hops[0].ref_column) + if ch.after.get(loc.hops[0].column) != key: + continue + if locate(schema, d, _as_change(ch.table, ch.after), d.values[p], max_hops=len(rel.hops)): + rows.append((d, ch.after)) + break + explained = {loc.hops[0].column, *rel.columns} + explained |= {h.column for h in rel.hops[:1]} + plan.children[p] = ChildGroup( + loc.hops[0].table, + loc.hops[0], + {p: rel}, + column_roles(schema, loc.hops[0].table, rows, explained, {}), + 1, + ) + for p in lists: + per_key: dict[str, list[list[Location]]] = {} + table = link = None + rows = [] + for d, a in zip(plan.demos, plan.anchors): + items = d.values.get(p) + if not isinstance(items, list): + continue + key = (a.after or {}).get(schema.tables[plan.anchor].primary_key[0]) if schema.tables[plan.anchor].primary_key else None + children = [ + ch + for ch in d.diff.changes + if ch.kind == "added" + and ch.after is not None + and ch.table != plan.anchor + and key is not None + and key in ch.after.values() + ] + used: set[int] = set() + for item in items: + if not isinstance(item, dict): + plan.refused.append(f"{p}: list items must be mappings") + return + for i, ch in enumerate(children): + if i in used: + continue + locs = { + k: locate(schema, d, _as_change(ch.table, ch.after), v, max_hops=1) + for k, v in item.items() + } + if all(locs.values()): + used.add(i) + link_col = next( + c for c, v in ch.after.items() if v == key + ) + table = ch.table + link = Hop( + "child", + ch.table, + link_col, + schema.tables[plan.anchor].primary_key[0], + ) + for k, ls in locs.items(): + per_key.setdefault(k, []).append(ls) + rows.append((d, ch.after)) + break + else: + plan.unresolved[f"{p} item {item}"] = [] + if table is None or link is None: + plan.unresolved.setdefault(p, []) + continue + locs = {} + for k, per in per_key.items(): + loc, tied = best(_intersect(per)) + if loc is None: + plan.unresolved[f"{p}.{k}"] = tied + return + locs[k] = loc + explained = {link.column} | {c for loc in locs.values() for c in loc.columns} + explained |= {loc.hops[0].column for loc in locs.values() if loc.hops} + plan.lists[p] = ChildGroup( + table, link, locs, column_roles(schema, table, rows, explained, {}), list_param=p + ) + + +def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> None: + """Classify every other row the demonstrations touched.""" + + child_tables = {g.table: p for p, g in plan.children.items()} + list_tables = {g.table for g in plan.lists.values()} + pure: dict[str, list[list[tuple[Demo, dict[str, Optional[str]]]]]] = {} + counts: dict[str, list[int]] = {} + for d, a in zip(plan.demos, plan.anchors): + added_keys: set[str] = set() + for ch in d.diff.changes: + if ch.kind == "added" and ch.after is not None: + t = schema.tables.get(ch.table) + if t is not None and len(t.primary_key) == 1: + v = ch.after.get(t.primary_key[0]) + if v is not None: + added_keys.add(v) + per_table: dict[str, list[dict[str, Optional[str]]]] = {} + for ch in d.diff.changes: + if ch is a or ch.table == plan.anchor and ch.key == a.key: + continue + if ch.kind == "removed": + if ch.table in child_tables or ch.table in list_tables: + continue + plan.removed[ch.table] = plan.removed.get(ch.table, 0) + 1 + continue + if ch.kind == "changed": + linked = ch.table in child_tables and a.after is not None and any( + v == (a.after or {}).get(plan_pk(schema, plan.anchor)) + for v in (ch.after or {}).values() + ) + if linked: + g = plan.children[child_tables[ch.table]] + g.changed_columns = sorted(set(g.changed_columns) | set(ch.changed_columns)) + elif ch.table == plan.anchor: + plan.bookkeeping.setdefault(ch.table, set()).update(ch.changed_columns) + else: + plan.bookkeeping.setdefault(ch.table, set()).update(ch.changed_columns) + continue + # added + if ch.table in list_tables: + continue + if ch.table in child_tables and _is_param_row(schema, plan, d, ch): + per_table.setdefault(ch.table, []) + counts.setdefault(ch.table, [0] * len(plan.demos)) + continue + if _is_ref_row(schema, plan, d, ch): + continue + per_table.setdefault(ch.table, []).append(ch.after or {}) + i = plan.demos.index(d) + for t, rows in per_table.items(): + counts.setdefault(t, [0] * len(plan.demos)) + counts[t][i] = max(len(rows), 1 if t in child_tables else 0) + pure.setdefault(t, []).append([(d, r) for r in rows]) + d.__dict__["_added_keys"] = added_keys + for t, cs in sorted(counts.items()): + rows = [r for group in pure.get(t, []) for r in group] + if ( + len(set(cs)) == 1 + and cs[0] > 0 + and not looks_like_log(t) + and rows + ): + links = [getattr(d, "_added_keys", set()) for d, _ in rows] + roles = column_roles(schema, t, rows, set(), actor_values, links) + plan.sides.append( + SideSpec(t, cs[0], roles, unless=child_tables.get(t)) + ) + elif rows: + plan.logs.add(t) + if plan.mode == "update": + for a in plan.anchors: + plan.anchor_changed |= set(a.changed_columns) + explained = {c for loc in plan.locations.values() if not loc.hops for c in loc.columns} + explained |= {loc.hops[0].column for loc in plan.locations.values() if loc.hops and loc.hops[0].kind == "fk"} + for locs in plan.dispatch_locations.values(): + for loc in locs.values(): + explained |= set(loc.columns) if not loc.hops else {loc.hops[0].column} + extra = plan.anchor_changed - explained + if extra: + plan.bookkeeping.setdefault(plan.anchor, set()).update(extra) + + +def plan_pk(schema: Schema, table: str) -> str: + pk = schema.tables[table].primary_key + return pk[0] if len(pk) == 1 else "" + + +def _is_param_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: + for p, g in plan.children.items(): + if g.table != ch.table or d.values.get(p) is None or ch.after is None: + continue + rel = g.params[p] + if locate(schema, d, _as_change(ch.table, ch.after), d.values[p], max_hops=len(rel.hops)): + return True + return False + + +def _is_ref_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: + """A row the write added that a parameter's location points to (a new + email address row a link row names): declared by the value it holds.""" + + if ch.after is None: + return False + for p, g in plan.children.items(): + rel = g.params[p] + if not rel.hops or rel.hops[0].table != ch.table or d.values.get(p) is None: + continue + col = rel.columns[0] if len(rel.columns) == 1 else None + if col and ch.after.get(col) is not None and _rule_ok(ch.after.get(col), d.values[p], rel.rule): + plan.added_refs.setdefault(p, {"table": ch.table, "column": col, "rule": rel.rule}) + return True + return False + + +def _rule_ok(stored: Any, value: Any, rule: str) -> bool: + from openadapt_flow.generate.demo_analysis import rule_for + + return rule_for(stored, "text", value) == rule + + +# -- SQL -------------------------------------------------------------------------- + + +class SqlBuilder: + """SQL text for one dialect. Every value from a task is bound as a + parameter; table and column names come from the schema and are quoted.""" + + def __init__(self, schema: Schema) -> None: + self.schema = schema + self.d = schema.dialect + self.params: dict[str, dict[str, str]] = {} + self._n = itertools.count() + + def q(self, name: str) -> str: + return store.quote(self.d, name) + + def ph(self, param: str) -> str: + for k, v in self.params.items(): + if v == {"param": param}: + return f"%({k})s" + name = f"v{next(self._n)}" + self.params[name] = {"param": param} + return f"%({name})s" + + def text(self, alias: str, table: str, column: str) -> str: + col = self.schema.tables[table].column(column) + ref = f"{alias}.{self.q(column)}" + kind = col.kind if col else "other" + if self.d == "mysql": + if kind in ("binary", "other"): + return f"HEX({ref})" + return f"CAST({ref} AS CHAR)" + if kind == "binary": + return f"encode({ref}, 'hex')" + return f"CAST({ref} AS TEXT)" + + def display(self, alias: str, table: str, columns: Sequence[str], rule: str) -> str: + if rule == "minutes": + a, b = (f"{alias}.{self.q(c)}" for c in columns) + if self.d == "mysql": + return f"(TIMESTAMPDIFF(SECOND, {a}, {b}) / 60)" + return f"(EXTRACT(EPOCH FROM ({b} - {a})) / 60)" + if rule in ("number", "date", "datetime"): + return f"{alias}.{self.q(columns[0])}" + if len(columns) == 1: + return self.text(alias, table, columns[0]) + parts = [self.text(alias, table, c) for c in columns] + if self.d == "mysql": + return "CONCAT(" + ", ' ', ".join(parts) + ")" + return " || ' ' || ".join(parts) + + def path(self, alias: str, table: str, loc: Location, depth: int = 0) -> str: + if not loc.hops: + return self.display(alias, table, loc.columns, loc.rule) + hop, rest = loc.hops[0], Location(loc.hops[1:], loc.columns, loc.rule) + if hop.kind != "fk": + raise GenerateError("a child hop can't be read as one value") + sub = f"a{depth + 1}" + inner = self.path(sub, hop.table, rest, depth + 1) + return ( + f"(SELECT {inner} FROM {self.q(hop.table)} {sub} WHERE " + f"{self.same(f'{sub}.{self.q(hop.ref_column)}', f'{alias}.{self.q(hop.column)}', hop.declared)})" + ) + + def same(self, a: str, b: str, declared: bool = True) -> str: + if declared: + return f"{a} = {b}" + if self.d == "mysql": + return f"CAST({a} AS BINARY) = CAST({b} AS BINARY)" + return f"CAST({a} AS TEXT) = CAST({b} AS TEXT)" + + def flag(self, expr: str, rule: str, ph: str) -> str: + if self.d == "mysql": + if rule in ("number", "minutes"): + cond = ( + f"CAST({ph} AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND " + f"{expr} = CAST({ph} AS DECIMAL(65,30))" + ) + elif rule == "date": + cond = f"{expr} = CAST({ph} AS DATE)" + else: + cond = f"{expr} = CAST({ph} AS DATETIME)" + else: + if rule in ("number", "minutes"): + cond = f"CAST({expr} AS numeric) = CAST({ph} AS numeric)" + elif rule == "date": + cond = f"{expr} = CAST({ph} AS date)" + else: + cond = f"{expr} = CAST({ph} AS timestamp)" + return f"CASE WHEN {cond} THEN '1' ELSE '0' END" + + def literal(self, text: str) -> str: + if "\\" in text or "\x00" in text: + raise GenerateError("a constant holds a backslash or NUL; not pinned") + return "'" + text.replace("'", "''") + "'" + + def pin(self, alias: str, table: str, column: str, value: Optional[str]) -> str: + """A flag: this column holds the demonstrated constant (or no value).""" + + col = self.schema.tables[table].column(column) + ref = f"{alias}.{self.q(column)}" + if value is None: + return f"CASE WHEN {ref} IS NULL THEN '1' ELSE '0' END" + kind = col.kind if col else "text" + if kind in ("number", "bool"): + cast = "DECIMAL(65,30)" if self.d == "mysql" else "numeric" + lhs = ref if self.d == "mysql" else f"CAST({ref} AS numeric)" + if kind == "bool" and self.d != "mysql": + lhs = f"CAST(CAST({ref} AS integer) AS numeric)" + value = "1" if value in ("1", "true", "True") else "0" + return f"CASE WHEN {lhs} = CAST({self.literal(value)} AS {cast}) THEN '1' ELSE '0' END" + if kind == "date": + return f"CASE WHEN {ref} = CAST({self.literal(value)} AS DATE) THEN '1' ELSE '0' END" + return f"CASE WHEN {self.text(alias, table, column)} = {self.literal(value)} THEN '1' ELSE '0' END" + + def empty(self, alias: str, table: str, column: str) -> str: + col = self.schema.tables[table].column(column) + ref = f"{alias}.{self.q(column)}" + if col is not None and col.kind == "text": + return f"CASE WHEN {ref} IS NULL OR {ref} = '' THEN '1' ELSE '0' END" + return f"CASE WHEN {ref} IS NULL THEN '1' ELSE '0' END" + + def copied(self, alias: str, detail: Mapping[str, Any], table: str, column: str) -> str: + src = ( + f"(SELECT s.{self.q(detail['column'])} FROM {self.q(detail['table'])} s " + f"WHERE s.{self.q(detail['ref_column'])} = {alias}.{self.q(detail['via'])})" + ) + ref = f"{alias}.{self.q(column)}" + return f"CASE WHEN {ref} = {src} THEN '1' ELSE '0' END" + + def key(self, alias: str, table: str) -> Optional[str]: + pk = self.schema.tables[table].primary_key + if not pk: + return None + if len(pk) == 1: + return self.text(alias, table, pk[0]) + parts = [self.text(alias, table, c) for c in pk] + if self.d == "mysql": + return "CONCAT_WS('|', " + ", ".join(parts) + ")" + return " || '|' || ".join(parts) + + +def _alias(name: str) -> str: + out = re.sub(r"[^A-Za-z0-9_]", "_", name) + return out[:60] + + +@dataclass +class Surface: + name: str + table: str + columns: list[tuple[str, str]] # (alias, expr) + alias: str = "t" + + def sql(self, b: SqlBuilder) -> str: + cols = ", ".join(f"{expr} AS {b.q(a)}" for a, expr in self.columns) + return f"SELECT {cols} FROM {b.q(self.table)} {self.alias}" + + +def _base_columns(b: SqlBuilder, table: str, alias: str = "t") -> list[tuple[str, str]]: + t = b.schema.tables[table] + cols: list[tuple[str, str]] = [] + key = b.key(alias, table) + if key is not None: + cols.append(("id", key)) + for c in t.columns: + if c.name == "id" and key is not None: + continue + cols.append((c.name, b.text(alias, table, c.name))) + return cols + + +# -- one contract ------------------------------------------------------------------ + + +@dataclass +class Variant: + kind: str + fields: list[str] + lists: dict[str, int] = field(default_factory=dict) + dispatch_value: Optional[str] = None + files: dict[str, str] = field(default_factory=dict) # create | found | update -> path + sample: dict[str, Any] = field(default_factory=dict) + + @property + def slug(self) -> str: + parts = sorted(self.fields) + [f"{k}x{n}" for k, n in sorted(self.lists.items())] + if self.dispatch_value is not None: + parts.insert(0, f"{self.dispatch_value}") + text = "+".join(parts) + digest = hashlib.sha256(text.encode()).hexdigest()[:8] + short = re.sub(r"[^A-Za-z0-9_+]", "_", text)[:80] + return f"{short}.{digest}" + + +@dataclass +class Settings: + read: dict[str, Any] + actor_params: dict[str, str] + recheck_after_s: Optional[float] + environment_id: str + max_window_s: int = 900 + #: Tables a view replaces in surface reads (the SQL guard refuses the + #: table's name): table -> view name. + views: dict[str, str] = field(default_factory=dict) + + +def _value_expr(param: str, rule: str) -> dict[str, Any]: + out: dict[str, Any] = {"param": param} + if rule in ("email", "phone"): + out["kind"] = rule + return out + + +def build_contract( + schema: Schema, + plan: KindPlan, + variant: Variant, + flavor: str, + settings: Settings, + bookkeeping: Mapping[str, set[str]], + logs: set[str], +) -> dict[str, Any]: + """One ``openadapt.verify/v1`` document for one field set.""" + + b = SqlBuilder(schema) + fields = set(variant.fields) + anchor = plan.anchor + surfaces: dict[str, Surface] = {} + effects: list[dict[str, Any]] = [] + target_fields: set[str] = set() + named_tables: set[str] = {anchor} + + # The main record, with every column and the values its parameters reach. + main = Surface("record", settings.views.get(anchor, anchor), _base_columns(b, anchor)) + match: dict[str, Any] = {} + locations = dict(plan.locations) + if variant.dispatch_value is not None: + locations.update(plan.dispatch_locations.get(variant.dispatch_value, {})) + identity_params = [] + for p in sorted(fields): + loc = locations.get(p) + if loc is None or (loc.hops and loc.hops[0].kind == "child"): + continue + identity_params.append(p) + key = _place(b, main, anchor, p, loc) + if loc.rule in ("number", "date", "datetime", "minutes"): + match[key] = {"literal": "1"} + else: + match[key] = _value_expr(p, loc.rule) + if plan.mode == "update" and p in plan.changes: + target_fields.add(key) + target_fields.update(loc.columns if not loc.hops else [loc.hops[0].column]) + if plan.mode == "create" and flavor == "create": + roles = _anchor_roles(schema, plan, variant, settings.actor_params) + for col, role in sorted(roles.items()): + _pin(b, main, anchor, col, role, match, settings.actor_params) + for p in plan.optional: + if p in fields or p in variant.lists: + continue + loc = plan.locations.get(p) + if loc is None: + continue + if not loc.hops: + for c in loc.columns: + main.columns.append((f"empty__{c}", b.empty("t", anchor, c))) + match[f"empty__{c}"] = {"literal": "1"} + elif loc.hops[0].kind == "fk": + c = loc.hops[0].column + main.columns.append((f"empty__{c}", b.empty("t", anchor, c))) + match[f"empty__{c}"] = {"literal": "1"} + effect: dict[str, Any] = { + "kind": "record_written", + "probe": "surface=record|the main record holds the task's values", + "match": match, + "expected_count": 1, + } + if plan.mode == "create" and flavor == "create": + effect["count_new_only"] = True + effects.append(effect) + surfaces["record"] = main + + # Rows beside the main record that hold a value (a note, a custom field, + # an email address link), and list items (invoice lines). + if flavor != "found": + groups = [(p, g) for p, g in plan.children.items() if p in fields] + groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] + for p, g in groups: + name = f"rows:{g.table}" + sname = _alias(name) + if sname not in surfaces: + cols = _base_columns(b, g.table) + link = g.link + # The main record's values, seen from the child row. + for ip in identity_params: + loc = locations[ip] + up = Location( + (Hop("fk", anchor, link.column, link.ref_column, True),) + loc.hops, + loc.columns, + loc.rule, + ) + expr = b.path("t", g.table, up) + if loc.rule in ("number", "date", "datetime", "minutes"): + expr = b.flag(expr, loc.rule, b.ph(ip)) + cols.append((f"main__{_alias(ip)}", expr)) + surfaces[sname] = Surface(sname, settings.views.get(g.table, g.table), cols) + named_tables.add(g.table) + surf = surfaces[sname] + items = range(variant.lists[p]) if g.list_param else [None] + for i in items: + m: dict[str, Any] = {} + for ip in identity_params: + loc = locations[ip] + if loc.rule in ("number", "date", "datetime", "minutes"): + m[f"main__{_alias(ip)}"] = {"literal": "1"} + else: + m[f"main__{_alias(ip)}"] = _value_expr(ip, loc.rule) + for k, loc in g.params.items(): + pname = k if g.list_param is None else f"{p}.{i}.{k}" + key = _place(b, surf, g.table, pname, loc) + if loc.rule in ("number", "date", "datetime", "minutes"): + m[key] = {"literal": "1"} + else: + m[key] = _value_expr(pname, loc.rule) + for col, role in sorted(g.roles.items()): + if role.role == "link": + continue + _pin(b, surf, g.table, col, role, m, settings.actor_params) + effects.append( + { + "kind": "record_written", + "probe": f"surface={sname}|a row beside the main record holds {p}", + "match": m, + "expected_count": 1, + "count_new_only": True, + } + ) + if plan.mode == "update": + target_fields.update(g.changed_columns) + # A new row a link row names (a new email address). + for p, ref in plan.added_refs.items(): + if p not in fields: + continue + effects.append(_raw_effect(b, schema, surfaces, settings, ref["table"], {ref["column"]: _value_expr(p, ref["rule"])}, 1, {}, f"the new {ref['table']} row holds {p}")) + for side in plan.sides: + if side.unless is not None and side.unless in fields: + continue + m = {} + for col, role in sorted(side.roles.items()): + if role.role == "constant" and role.value is not None: + m[col] = {"literal": role.value} + elif role.role == "actor": + m[col] = {"param": role.detail["param"]} + effects.append( + _raw_effect(b, schema, surfaces, settings, side.table, m, side.count, side.roles, f"{side.count} row(s) the app adds to {side.table}") + ) + + verifier: dict[str, Any] = {"kind": "surfaces", "surfaces": {}} + for s in surfaces.values(): + sub: dict[str, Any] = {"kind": "sql", **settings.read, "sql_query": s.sql(b)} + used = {k: v for k, v in b.params.items() if f"%({k})s" in sub["sql_query"]} + if used: + sub["sql_query_params"] = used + verifier["surfaces"][s.name] = sub + # Main and child effects read named surfaces: no other read is judged + # against them, and the whole-database frame covers every other table. + ignore = sorted({c for cols in bookkeeping.values() for c in cols} & _all_columns(schema, named_tables)) + frame: dict[str, Any] = {} + if ignore: + frame["ignore_fields"] = ignore + if plan.mode == "create" or flavor == "found": + frame["target_fields"] = [] + else: + frame["target_fields"] = sorted(target_fields) + if schema.dialect == "mysql": + allow: list[dict[str, Any]] = [] + for t in sorted(logs): + if t in named_tables: + continue + allow.append({"table": t}) + for t in sorted(named_tables): + allow.append( + { + "table": t, + "mode": "skip", + "reason": "read in full, every column, by this contract's own surface", + } + ) + views = { + s.table: s.name for s in surfaces.values() if s.name.startswith("raw_") + } + db: dict[str, Any] = { + "dialect": "mysql", + "read": dict(settings.read), + "allow_tables": allow, + "views": views, + "settle_s": 1, + "settle_attempts": 5, + } + nrf = { + s.table: sorted(getattr(s, "_new_row_fields", [])) + for s in surfaces.values() + if s.name.startswith("raw_") and getattr(s, "_new_row_fields", None) + } + if nrf: + db["new_row_fields"] = nrf + ign = {t: sorted(c) for t, c in bookkeeping.items() if t not in named_tables and c} + if ign: + db["ignore_columns"] = ign + if settings.recheck_after_s: + db["recheck_after_s"] = settings.recheck_after_s + frame["database"] = db + else: + # PostgreSQL: frame.database reads MySQL and SQLite only, so every + # other table is read as its own surface (the read-set frame). + for t in sorted(schema.tables): + if t in named_tables or t in logs: + continue + if f"raw_{_alias(t)}" in surfaces: + continue + s = Surface(f"raw_{_alias(t)}", settings.views.get(t, t), _base_columns(b, t)) + surfaces[s.name] = s + verifier["surfaces"][s.name] = {"kind": "sql", **settings.read, "sql_query": s.sql(b)} + doc: dict[str, Any] = { + "schema_version": "openadapt.verify/v1", + "environment_id": settings.environment_id, + "max_execution_window_s": settings.max_window_s, + "verifier": verifier, + "effects": effects, + "frame": frame, + } + return doc + + +def _all_columns(schema: Schema, tables: set[str]) -> set[str]: + return {c.name for t in tables if t in schema.tables for c in schema.tables[t].columns} + + +def _place(b: SqlBuilder, surf: Surface, table: str, param: str, loc: Location) -> str: + """Add the column that holds ``param``'s value to ``surf``; its name.""" + + if not loc.hops and len(loc.columns) == 1 and loc.rule in ("exact", "email", "phone"): + return loc.columns[0] if loc.columns[0] != "id" else "id" + expr = b.path("t", table, loc) + alias = f"p__{_alias(param)}" + if loc.rule in ("number", "date", "datetime", "minutes"): + expr = b.flag(expr, loc.rule, b.ph(param)) + if alias not in {a for a, _ in surf.columns}: + surf.columns.append((alias, expr)) + return alias + + +def _pin( + b: SqlBuilder, + surf: Surface, + table: str, + col: str, + role: ColumnRole, + match: dict[str, Any], + actor_params: Mapping[str, str], +) -> None: + if role.role == "constant": + column = b.schema.tables[table].column(col) + if role.value is not None and column is not None and column.kind == "text": + match.setdefault(col, {"literal": role.value}) + return + alias = f"pin__{_alias(col)}" + surf.columns.append((alias, b.pin("t", table, col, role.value))) + match[alias] = {"literal": "1"} + elif role.role == "actor": + match.setdefault(col, {"param": role.detail["param"]}) + elif role.role == "copied": + alias = f"pin__{_alias(col)}" + surf.columns.append((alias, b.copied("t", role.detail, table, col))) + match[alias] = {"literal": "1"} + + +def _raw_effect( + b: SqlBuilder, + schema: Schema, + surfaces: dict[str, Surface], + settings: Settings, + table: str, + match: dict[str, Any], + count: int, + roles: Mapping[str, ColumnRole], + note: str, +) -> dict[str, Any]: + """An effect on a table's own read: rows the app adds beside the write. + + On MySQL the table is judged by the whole-database frame, which also + checks that every column these rows set beyond ``match`` is declared in + ``new_row_fields`` or holds its default. On PostgreSQL the surface reads + every column as text, so constants are pinned in ``match`` itself.""" + + name = f"raw_{_alias(table)}" + surf = surfaces.get(name) + if surf is None: + surf = Surface(name, settings.views.get(table, table), _base_columns(b, table)) + surfaces[name] = surf + t = schema.tables[table] + m = dict(match) + if schema.dialect == "mysql": + # Text and whole-number constants only: the snapshot's JSON form of + # a decimal or a date isn't the generator's text form. + for col, value in list(m.items()): + c = t.column(col) + lit = value.get("literal") if isinstance(value, dict) else None + if c is not None and lit is not None and not ( + c.kind == "text" or (c.kind in ("number", "bool") and re.fullmatch(r"-?\d+", lit)) + ): + m.pop(col) + nrf = set(getattr(surf, "_new_row_fields", set())) + for c in t.columns: + if c.name in m: + continue + role = roles.get(c.name) + if role is not None and role.role == "constant" and role.value is None: + continue + if role is None or role.role != "constant" or (role.detail or {}).get("is_default"): + if role is None or role.role in ("key", "timestamp", "link", "varies", "copied", "actor"): + nrf.add(c.name) + continue + nrf.add(c.name) + surf.__dict__["_new_row_fields"] = nrf + else: + for col, role in sorted(roles.items()): + if role.role == "constant" and col not in m: + if role.value is None: + alias = f"pin__{_alias(col)}" + if alias not in {a for a, _ in surf.columns}: + surf.columns.append((alias, b.pin("t", table, col, None))) + m[alias] = {"literal": "1"} + else: + m[col] = {"literal": role.value} + return { + "kind": "record_written", + "probe": f"surface={name}|{note}", + "match": m, + "expected_count": count, + "count_new_only": True, + } + + +def _anchor_roles( + schema: Schema, plan: KindPlan, variant: Variant, actor_params: Mapping[str, str] +) -> dict[str, ColumnRole]: + """The main row's columns no parameter explains, from the demonstrations + that give this field set (all of them when none gives exactly it).""" + + fields = set(variant.fields) | set(variant.lists) + if variant.dispatch_value is not None: + fields.add(plan.dispatch or "") + support = [ + (d, a) + for d, a in zip(plan.demos, plan.anchors) + if fields <= {k for k, v in d.values.items() if v is not None} + ] or list(zip(plan.demos, plan.anchors)) + explained: set[str] = set() + for loc in plan.locations.values(): + if not loc.hops: + explained |= set(loc.columns) + elif loc.hops[0].kind == "fk": + explained.add(loc.hops[0].column) + rows = [(d, a.after or {}) for d, a in support] + return column_roles(schema, plan.anchor, rows, explained, actor_params) + + +# -- variants ----------------------------------------------------------------------- + + +def variants(schema: Schema, plan: KindPlan, spec: KindSpec, answers: Mapping[str, Any]) -> list[Variant]: + out: list[Variant] = [] + lists = list(plan.lists) + if plan.mode == "update": + if plan.dispatch is not None: + values = dict(plan.dispatch_locations) + # By analogy: another column of the main table, written the way + # a demonstrated plain column is. + plain = [ + loc + for locs in values.values() + for loc in locs.values() + if not loc.hops and loc.rule == "exact" + ] + if plain: + vp = next(iter(next(iter(values.values())))) + for c in schema.tables[plan.anchor].columns: + for name in {c.name, _camel(c.name)}: + if name in values or c.name in schema.tables[plan.anchor].primary_key: + continue + if c.kind == "text": + values[name] = {vp: Location((), (c.name,), "exact")} + plan.dispatch_locations = values + for dv in sorted(values): + fields = sorted(set(plan.identity) | {plan.dispatch, *values[dv]}) + out.append(Variant(plan.kind, fields, dispatch_value=dv)) + return out + changes = [p for p in plan.changes if p in plan.locations] + for n in range(1, len(changes) + 1): + for combo in itertools.combinations(changes, n): + out.append(Variant(plan.kind, sorted(set(plan.identity) | set(combo)))) + return out[:MAX_VARIANTS] + located = set(plan.locations) + base_sets = [] + for d in plan.demos: + given = { + k for k, v in d.values.items() if v is not None and not isinstance(v, list) + } & (located | set(plan.children)) + base_sets.append(frozenset(given)) + optional = [p for p in plan.optional if p in located and p not in lists] + seen: set[frozenset[str]] = set() + sets: list[frozenset[str]] = [] + for base in base_sets: + drop = [p for p in optional if p in base] + for n in range(0, len(drop) + 1): + for combo in itertools.combinations(drop, n): + s = frozenset(base - set(combo)) + if s not in seen: + seen.add(s) + sets.append(s) + # A field the examples leave out sometimes, and that one demonstration + # gives, may be added to a set from another demonstration. + for s in list(sets): + for p in optional: + if p not in s: + t = frozenset(s | {p}) + if t not in seen: + seen.add(t) + sets.append(t) + sets = sets[:MAX_VARIANTS] + for s in sets: + if lists: + for n in range(1, MAX_LIST + 1): + out.append(Variant(plan.kind, sorted(s), lists={lists[0]: n})) + else: + out.append(Variant(plan.kind, sorted(s))) + return out + + +def _camel(name: str) -> str: + head, *rest = name.split("_") + return head + "".join(w.title() for w in rest) + + +# -- validation ------------------------------------------------------------------------ + + +def flatten(values: Mapping[str, Any]) -> dict[str, Any]: + """Task values as the contracts bind them: ``lines: [{qty: 4}]`` is + ``lines.0.qty: 4``; nulls are left out.""" + + out: dict[str, Any] = {} + for k, v in values.items(): + if v is None: + continue + if isinstance(v, list): + for i, item in enumerate(v): + if isinstance(item, dict): + for ik, iv in item.items(): + if iv is not None: + out[f"{k}.{i}.{ik}"] = iv + else: + out[f"{k}.{i}"] = item + else: + out[k] = v + return out + + +def sample_values(plan: KindPlan, variant: Variant) -> dict[str, Any]: + """Values to dry-read a contract with: the demonstration's own.""" + + out: dict[str, Any] = {} + for d in plan.demos: + for k, v in d.values.items(): + if v is not None and k not in out: + out[k] = v + if variant.dispatch_value is not None and plan.dispatch: + out[plan.dispatch] = variant.dispatch_value + for p, n in variant.lists.items(): + items = out.get(p) if isinstance(out.get(p), list) else [] + if items: + out[p] = [items[i % len(items)] for i in range(n)] + keep = set(variant.fields) | set(variant.lists) + return {k: v for k, v in out.items() if k in keep} + + +def dry_read(path: Path, params: Mapping[str, Any], mysql: bool) -> Optional[str]: + """Load the contract and read the live store once, as ``begin_write`` + would. None when it reads; else why not.""" + + from openadapt_flow.verifier import Contract, VerifierError + from openadapt_flow.verify_run import FRAME_DB_DETAIL, VerifyRunError, capture_baseline + + try: + contract = Contract.load(path) + except VerifierError as exc: + return f"does not load: {exc}" + with tempfile.TemporaryDirectory() as tmp: + try: + payload = capture_baseline(contract.config, params, Path(tmp) / "b.json") + except (VerifyRunError, ValueError, KeyError) as exc: + return f"dry read failed: {exc}" + state = payload.get("state", {}) + if not state.get("reachable"): + return "dry read: the store could not be read" + detail = state.get("detail") or {} + for name, sub in detail.items(): + if isinstance(sub, dict) and "reachable" in sub and not sub.get("reachable"): + if name == "openadapt:frame_audit": + continue + return f"dry read: surface {name!r} could not be read" + if mysql: + snap = detail.get(FRAME_DB_DETAIL) + if not isinstance(snap, dict): + return "dry read: the whole-database frame could not be read" + if snap.get("code_visible") is False: + return ( + "dry read: the read account can't see triggers (MySQL needs the " + "TRIGGER privilege on the database), so the frame can't be checked" + ) + return None + + +def check_read_only(connect: Any, dialect: str) -> list[str]: + """Why the read account is not read-only (empty when it is).""" + + problems: list[str] = [] + conn = connect() + try: + cur = conn.cursor() + if dialect == "mysql": + cur.execute("SHOW GRANTS") + for (grant,) in cur.fetchall(): + g = str(grant).upper() + privs = g.split(" ON ")[0].replace("GRANT ", "") + for word in ( + "ALL PRIVILEGES", + "INSERT", + "UPDATE", + "DELETE", + "CREATE", + "DROP", + "ALTER", + "INDEX", + "EXECUTE", + "FILE", + "SUPER", + "EVENT", + ): + if re.search(rf"\b{word}\b", privs) and not privs.startswith("USAGE"): + problems.append(f"the read account has {word}: {grant}") + else: + cur.execute( + "SELECT rolsuper, rolcreaterole, rolcreatedb FROM pg_roles " + "WHERE rolname = current_user" + ) + row = cur.fetchone() + if row and any(row): + problems.append("the read account is a superuser or can create roles or databases") + cur.execute( + "SELECT table_name FROM information_schema.role_table_grants " + "WHERE grantee = current_user AND privilege_type IN " + "('INSERT','UPDATE','DELETE','TRUNCATE') AND table_schema = current_schema() LIMIT 5" + ) + for (t,) in cur.fetchall(): + problems.append(f"the read account can write table {t}") + finally: + conn.rollback() + conn.close() + return problems + + +def guard_problem(sql: str) -> Optional[str]: + from openadapt_flow.runtime.effects.sql import assert_read_only_sql + + try: + assert_read_only_sql(sql) + except ValueError as exc: + return str(exc) + return None + + +def create_views( + admin_read: Mapping[str, Any], schema: Schema, tables: Sequence[str], reader: str +) -> dict[str, str]: + """Create one read-only view per table the SQL guard refuses by name, + in the same database, and grant the read account SELECT on it. Only run + with ``--create-views``; the connection is the admin one given.""" + + import importlib + + module = importlib.import_module(str(admin_read["sql_driver"])) + args = dict(admin_read.get("sql_connect_args") or {}) + env = admin_read.get("sql_password_env") + if env: + if env not in os.environ: + raise GenerateError(f"environment variable {env} (the admin password) is not set") + args["password"] = os.environ[str(env)] + conn = module.connect(**args) + made = {} + try: + cur = conn.cursor() + for t in tables: + view = f"oa_read_{t}" + q = store.quote(schema.dialect, t) + v = store.quote(schema.dialect, view) + cur.execute(f"CREATE OR REPLACE VIEW {v} AS SELECT * FROM {q}") + if schema.dialect == "mysql": + cur.execute(f"GRANT SELECT ON {v} TO {reader}") + else: + cur.execute(f"GRANT SELECT ON {v} TO {store.quote('postgresql', reader)}") + made[t] = view + conn.commit() + finally: + conn.close() + return made + + +# -- the whole run -------------------------------------------------------------------- + + +def propose( + *, + read: dict[str, Any], + kinds: list[KindSpec], + demo_dir: Path, + out: Path, + actor_params: Mapping[str, str], + answers: Mapping[str, Any], + environment_id: str = "", + admin_read: Optional[Mapping[str, Any]] = None, + create_views_flag: bool = False, + reader_account: str = "", + validate: bool = True, +) -> dict[str, Any]: + """Analyze, ask, emit and validate. Returns the report (also written).""" + + connect = store.connector(read) + dialect = store.dialect_of(read) + schema = store.read_schema(connect, dialect) + problems = check_read_only(connect, dialect) + if problems: + raise GenerateError( + "the read account is not read-only, so no contract is proposed: " + + "; ".join(problems) + ) + demos = load_demos(demo_dir) + env = environment_id or f"generated-{schema.database}" + env = re.sub(r"[^A-Za-z0-9._:-]", "-", env) + if len(env) < 8: + env = (env + "-environment")[:64] + plans = [analyze(schema, k, demos, actor_params) for k in kinds] + questions: list[Question] = [] + + # Q: bookkeeping, for the whole app at once. + logs = set().union(*(p.logs for p in plans)) if plans else set() + book: dict[str, set[str]] = {} + unchecked: dict[str, set[str]] = {} + for p in plans: + for t, cols in p.bookkeeping.items(): + book.setdefault(t, set()).update(cols) + if p.mode == "create" and p.demos: + for v in [Variant(p.kind, sorted(p.locations))]: + for col, role in _anchor_roles(schema, p, v, actor_params).items(): + if role.role == "varies": + unchecked.setdefault(p.anchor, set()).add(col) + ans = _answer(answers, "bookkeeping") + if isinstance(ans, dict): + logs = (logs | set(ans.get("logs_add", []))) - set(ans.get("logs_remove", [])) + for t, cols in dict(ans.get("columns_add", {})).items(): + book.setdefault(t, set()).update(cols) + for t, cols in dict(ans.get("columns_remove", {})).items(): + book[t] = book.get(t, set()) - set(cols) + if logs or book or unchecked: + lines = [] + if logs: + lines.append( + "Tables treated as the app's own logs (rows may be added, existing " + "rows must not change" + + (")" if dialect == "mysql" else "; on PostgreSQL they are not read at all)") + + ": " + + ", ".join(sorted(logs)) + ) + if book: + lines.append( + "Columns that change on their own and are not compared: " + + "; ".join(f"{t}: {', '.join(sorted(c))}" for t, c in sorted(book.items()) if c) + ) + if unchecked: + lines.append( + "Columns of a new record that differ between demonstrations and are " + "not checked: " + + "; ".join(f"{t}: {', '.join(sorted(c))}" for t, c in sorted(unchecked.items())) + ) + questions.append( + Question( + "bookkeeping", + "Is this the app's own bookkeeping, which a write may change without " + "a task asking? " + " ".join(lines) + + " Answer `bookkeeping: accept`, or list changes under " + "logs_add, logs_remove, columns_add, columns_remove.", + "accept", + ) + ) + # Q: the recheck window (MySQL: frame.database.recheck_after_s). + recheck: Optional[float] = None + if dialect == "mysql": + a = _answer(answers, "recheck_after_s") + recheck = float(a) if isinstance(a, (int, float)) and a > 0 else None + questions.append( + Question( + "recheck_after_s", + "How long after a verified write can the app still write on its own " + "(a background job, a delayed retry)? The database is read again that " + f"many seconds later and a late change refutes the write. Proposed: " + f"{DEFAULT_RECHECK_S}. Answer `recheck_after_s: `.", + DEFAULT_RECHECK_S, + ) + ) + # Q: per kind, a value the generator couldn't place. + for p in plans: + if not p.unresolved: + continue + chosen = dict(_answer(answers, f"locations.{p.kind}") or {}) + open_ = {} + for name, tied in p.unresolved.items(): + pick = chosen.get(name) + if isinstance(pick, int) and 0 <= pick < len(tied): + p.locations[name] = tied[pick] + if p.mode == "update" and name not in p.identity + p.changes: + p.changes.append(name) + else: + open_[name] = tied + p.unresolved = open_ + if open_: + parts = [] + for name, tied in open_.items(): + if tied: + opts = "; ".join(f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied)) + parts.append(f"`{name}` is in several places: {opts}") + else: + parts.append( + f"`{name}` was not found in any row the demonstrations wrote; " + "tasks that give it are stopped" + ) + questions.append( + Question( + f"locations.{p.kind}", + f"Kind `{p.kind}`: " + " ".join(parts) + + f" Answer `locations.{p.kind}: {{: }}` to pick one.", + None, + ) + ) + questions_open = [ + q for q in questions if _answer(answers, q.key) is None + ] + settings = Settings(read, dict(actor_params), recheck or (DEFAULT_RECHECK_S if dialect == "mysql" else None), env) + + # Tables whose name the SQL guard refuses: a view, or a refusal. + blocked = sorted( + t + for t in schema.tables + if guard_problem(f"SELECT * FROM {store.quote(dialect, t)} t") is not None + ) + needed = sorted( + {p.anchor for p in plans if p.anchor in blocked} + | {g.table for p in plans for g in list(p.children.values()) + list(p.lists.values()) if g.table in blocked} + ) + if needed: + existing = {t: f"oa_read_{t}" for t in needed if f"oa_read_{t}" in schema.views} + missing = [t for t in needed if t not in existing] + if missing and create_views_flag: + if not admin_read or not reader_account: + raise GenerateError("--create-views needs --admin-read and --reader-account") + existing.update(create_views(admin_read, schema, missing, reader_account)) + schema = store.read_schema(connect, dialect) + for t, v in list(existing.items()): + if v not in schema.views: + existing.pop(t) + settings.views = existing + for p in plans: + tables = {p.anchor} | {g.table for g in list(p.children.values()) + list(p.lists.values())} + for t in sorted(tables & set(blocked)): + if t not in existing: + p.refused.append( + f"the SQL guard refuses the table name {t!r} in a read; rerun " + "with --create-views --admin-read --reader-account " + " to read it through a view, or wait for the guard fix" + ) + + out.mkdir(parents=True, exist_ok=True) + manifest: dict[str, Any] = { + "schema_version": MANIFEST_SCHEMA, + "dialect": dialect, + "database": schema.database, + "actor_params": sorted(actor_params), + "questions_open": [q.key for q in questions_open], + "kinds": {}, + } + report: dict[str, Any] = { + "dialect": dialect, + "tables": len(schema.tables), + "questions": [ + {"number": i + 1, "key": q.key, "text": q.text, "proposal": q.proposal, "answered": _answer(answers, q.key) is not None} + for i, q in enumerate(questions) + ], + "kinds": {}, + } + for p, spec in zip(plans, kinds): + entry: dict[str, Any] = { + "mode": p.mode, + "main_table": p.anchor, + "parameters": p.params, + "identity": sorted(p.identity), + "lists": sorted(p.lists), + "dispatch": p.dispatch, + "variants": [], + } + rep: dict[str, Any] = { + "mode": p.mode, + "main_table": p.anchor, + "locations": {k: v.describe(p.anchor) for k, v in sorted(p.locations.items())}, + "children": {k: g.table for k, g in p.children.items()}, + "lists": {k: g.table for k, g in p.lists.items()}, + "side_rows": [{"table": s.table, "count": s.count, "unless": s.unless} for s in p.sides], + "logs": sorted(p.logs), + "refused": list(p.refused), + "unresolved": {k: [loc.describe(p.anchor) for loc in v] for k, v in sorted(p.unresolved.items())}, + "emitted": 0, + "rejected": [], + } + if p.removed: + rep["refused"].append( + "the demonstrations removed rows the generator can't explain: " + + ", ".join(f"{t} ({n})" for t, n in sorted(p.removed.items())) + ) + if p.refused or not p.mode: + manifest["kinds"][p.kind] = entry + report["kinds"][p.kind] = rep + continue + flavors = ["create", "found"] if p.mode == "create" else ["update"] + for v in variants(schema, p, spec, answers): + if any(f not in p.locations and f != p.dispatch and f not in p.children and f not in (p.dispatch_locations.get(v.dispatch_value or "", {})) for f in v.fields): + continue + ok = True + for flavor in flavors: + doc = build_contract(schema, p, v, flavor, settings, book, logs) + rel = Path("contracts") / p.kind / f"{v.slug}.{flavor}.yaml" + path = out / rel + path.parent.mkdir(parents=True, exist_ok=True) + header = ( + f"# Generated by openadapt_flow.generate.verify_contract: kind {p.kind}, " + f"{flavor}, fields {', '.join(v.fields + [f'{k}x{n}' for k, n in v.lists.items()])}.\n" + "# Proposed, not admitted: a person answers QUESTIONS.md first.\n" + ) + path.write_text(header + yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=100), encoding="utf-8") + why = None + for s in doc["verifier"]["surfaces"].values(): + why = why or guard_problem(s["sql_query"]) + if why is None and validate: + why = dry_read(path, flatten(sample_values(p, v)), dialect == "mysql") + if why is not None: + path.unlink() + rep["rejected"].append({"variant": v.slug, "flavor": flavor, "why": why}) + ok = False + break + v.files[flavor] = str(rel) + if ok: + rep["emitted"] += 1 + entry["variants"].append( + { + "fields": v.fields, + "lists": v.lists, + "dispatch_value": v.dispatch_value, + **{k: f for k, f in v.files.items()}, + } + ) + else: + for f in v.files.values(): + (out / f).unlink(missing_ok=True) + manifest["kinds"][p.kind] = entry + report["kinds"][p.kind] = rep + (out / "manifest.yaml").write_text(yaml.safe_dump(manifest, sort_keys=False, allow_unicode=True), encoding="utf-8") + (out / "report.json").write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8") + (out / "QUESTIONS.md").write_text(_questions_md(questions, answers, report), encoding="utf-8") + report["questions_open"] = [q.key for q in questions_open] + return report + + +def _questions_md(questions: Sequence[Question], answers: Mapping[str, Any], report: Mapping[str, Any]) -> str: + lines = ["# Questions for a person", ""] + if not questions: + lines.append("None: every decision followed from the schema and the demonstrations.") + for i, q in enumerate(questions, 1): + done = _answer(answers, q.key) + lines.append(f"{i}. {q.text}") + lines.append(f" - Answer key: `{q.key}`. Proposed: `{q.proposal}`.") + lines.append(f" - {'Answered: `' + json.dumps(done, ensure_ascii=False) + '`' if done is not None else 'Not answered yet.'}") + lines.append("") + lines.append("## Kinds") + lines.append("") + for kind, rep in report["kinds"].items(): + lines.append(f"- `{kind}` ({rep['mode'] or 'no contract'}, main table `{rep['main_table']}`): {rep['emitted']} contract(s).") + for r in rep["refused"]: + lines.append(f" - Refused: {r}") + for r in rep["rejected"][:5]: + lines.append(f" - Not emitted ({r['variant']}, {r['flavor']}): {r['why']}") + return "\n".join(lines) + "\n" + + +# -- at run time --------------------------------------------------------------------------- + + +def choose( + manifest_path: Path, + kind: str, + values: Mapping[str, Any], + extra_params: Optional[Mapping[str, Any]] = None, + *, + require_answers: bool = True, +) -> tuple[Any, dict[str, Any]]: + """The contract for one task, and the params to call ``begin_write`` with. + + The field set is the task's non-null values (a list counts by length). + For a create kind, the store is read first: when a record with the + task's values already exists the ``found`` contract applies (nothing may + change), else the ``create`` one. Raises :class:`GenerateError` when no + contract fits: the write is then stopped, never sent unchecked. + """ + + from openadapt_flow.verifier import Contract + + manifest_path = Path(manifest_path) + manifest = load_yaml(manifest_path) or {} + if manifest.get("schema_version") != MANIFEST_SCHEMA: + raise GenerateError(f"{manifest_path} is not a contract manifest") + if require_answers and manifest.get("questions_open"): + raise GenerateError( + "the manifest still has open questions: " + + ", ".join(manifest["questions_open"]) + + " (answer them in answers.yaml and run propose again)" + ) + entry = (manifest.get("kinds") or {}).get(kind) + if not entry or not entry.get("variants"): + raise GenerateError(f"no contract for kind {kind!r}") + given = {k for k, v in values.items() if v is not None and not isinstance(v, list)} + lists = {k: len(v) for k, v in values.items() if isinstance(v, list)} + dispatch = entry.get("dispatch") + for var in entry["variants"]: + if set(var["fields"]) != given or dict(var.get("lists") or {}) != lists: + continue + if dispatch and str(values.get(dispatch)) != str(var.get("dispatch_value")): + continue + params = {**flatten(values), **dict(extra_params or {})} + base = manifest_path.parent + if entry["mode"] == "update": + return Contract.load(base / var["update"]), params + found = Contract.load(base / var["found"]) + if _exists(found, params): + return found, params + return Contract.load(base / var["create"]), params + raise GenerateError( + f"no {kind} contract for the fields {sorted(given)}" + + (f" and lists {lists}" if lists else "") + + ": record a demonstration that gives them, or the task is stopped" + ) + + +def _exists(contract: Any, params: Mapping[str, Any]) -> bool: + """Does a record matching the found contract's main effect exist now?""" + + from openadapt_flow.runtime.effects.effect import record_matches + from openadapt_flow.verify_run import _build_verifier + + verifier = _build_verifier(contract.config, params) + state = verifier.capture_pre_state() + effect = contract.config.effects[0].resolve(params) + sub = (state.detail or {}).get("record", {}) + if not sub.get("reachable"): + raise GenerateError("the store could not be read to choose the contract") + selector = {k: v for k, v in effect.match.items()} + return any(record_matches(dict(r), selector) for r in sub.get("records", [])) + + +# -- CLI ------------------------------------------------------------------------------------ + + +def _now() -> str: + return _dt.datetime.now(_dt.timezone.utc).replace(tzinfo=None).isoformat(timespec="seconds") + + +def _parse_actor(items: Sequence[str]) -> dict[str, str]: + out = {} + for item in items or []: + if "=" not in item: + raise GenerateError(f"--actor-param {item!r}: use NAME=VALUE") + k, v = item.split("=", 1) + out[k.strip()] = v.strip() + return out + + +def main(argv: Optional[Sequence[str]] = None) -> int: + ap = argparse.ArgumentParser( + prog="python -m openadapt_flow.generate.verify_contract", + description=( + "Propose openadapt.verify/v1 contracts for an app's kinds of write, from " + "its SQL schema (MySQL, MariaDB or PostgreSQL) and one demonstration " + "write per example. Steps: `schema` (check the read account), then for " + "each example `snapshot` before the write and `record` after it, then " + "`propose`. Answer QUESTIONS.md in answers.yaml and run `propose` again. " + "Guide: packages/flow/docs/GENERATE_CONTRACTS.md. For a FHIR ensure-record " + "contract use openadapt_flow.generate.ensure_contract instead." + ), + ) + sub = ap.add_subparsers(dest="command", required=True) + s = sub.add_parser("schema", help="read the schema and check the account is read-only") + s.add_argument("--read", required=True, type=Path, help="read.yaml: sql_driver, sql_connect_args, sql_password_env") + s.add_argument("--out", type=Path, help="write schema.json here") + s = sub.add_parser("snapshot", help="snapshot every table before a demonstration write") + s.add_argument("--read", required=True, type=Path) + s.add_argument("--out", required=True, type=Path, help="e.g. work/before.json (holds row values: keep it private)") + s = sub.add_parser("record", help="snapshot again after the write and save the demonstration") + s.add_argument("--read", required=True, type=Path) + s.add_argument("--before", required=True, type=Path) + s.add_argument("--kind", required=True) + s.add_argument("--values", required=True, help="the task's values as JSON, or @file.json") + s.add_argument("--demos", required=True, type=Path, help="directory of demonstrations") + s.add_argument("--name", default="", help="file name stem (default: kind and a number)") + s = sub.add_parser("propose", help="analyze the demonstrations and write contracts") + s.add_argument("--read", required=True, type=Path) + s.add_argument("--kinds", required=True, type=Path, help="kinds.yaml: kinds, parameters, examples") + s.add_argument("--demos", required=True, type=Path) + s.add_argument("--out", required=True, type=Path) + s.add_argument("--answers", type=Path, help="answers.yaml with the person's answers") + s.add_argument("--actor-param", action="append", default=[], help="NAME=VALUE the caller passes for the acting user (e.g. bot_user_id=5); a column holding it is the actor") + s.add_argument("--environment-id", default="") + s.add_argument("--create-views", action="store_true", help="create a view for a table the SQL guard refuses by name (needs --admin-read)") + s.add_argument("--admin-read", type=Path, help="admin.yaml: a connection allowed to CREATE VIEW and GRANT") + s.add_argument("--reader-account", default="", help="the read account to GRANT SELECT on created views (MySQL: 'user'@'%%')") + s.add_argument("--no-validate", action="store_true", help="skip the dry read (tests only)") + args = ap.parse_args(argv) + try: + read = load_read(args.read) + connect = store.connector(read) + dialect = store.dialect_of(read) + if args.command == "schema": + schema = store.read_schema(connect, dialect) + problems = check_read_only(connect, dialect) + if args.out: + args.out.write_text(json.dumps(schema.to_json(), indent=1), encoding="utf-8") + print(f"{dialect} {schema.database}: {len(schema.tables)} tables, {len(schema.views)} views") + for p in problems: + print(f"NOT READ-ONLY: {p}") + return 1 if problems else 0 + if args.command == "snapshot": + schema = store.read_schema(connect, dialect) + rows = store.snapshot(connect, schema) + store.save_snapshot(args.out, rows) + (args.out.with_suffix(".meta.json")).write_text(json.dumps({"taken_at": _now()}), encoding="utf-8") + print(f"snapshot of {len(rows)} tables -> {args.out}") + return 0 + if args.command == "record": + schema = store.read_schema(connect, dialect) + values = args.values + if values.startswith("@"): + values = Path(values[1:]).read_text(encoding="utf-8") + values = json.loads(values) + before = store.load_snapshot(args.before) + meta = args.before.with_suffix(".meta.json") + started = json.loads(meta.read_text())["taken_at"] if meta.exists() else "" + after = store.snapshot(connect, schema) + d = store.diff(before, after) + args.demos.mkdir(parents=True, exist_ok=True) + n = len(list(args.demos.glob(f"{args.kind}-*.demo.json"))) + 1 + stem = args.name or f"{args.kind}-{n}" + snap = f"{stem}.after.json" + store.save_snapshot(args.demos / snap, after) + doc = { + "kind": args.kind, + "values": values, + "started_at": started, + "finished_at": _now(), + "after_snapshot": snap, + "diff": d.to_json(), + } + path = args.demos / f"{stem}.demo.json" + path.write_text(json.dumps(doc, indent=1, ensure_ascii=False), encoding="utf-8") + os.chmod(path, 0o600) + print(f"{path}: {len(d.changes)} row change(s) in {', '.join(d.tables()) or 'no table'}") + return 0 + answers = load_yaml(args.answers) if args.answers else {} + admin = load_read(args.admin_read) if args.admin_read else None + report = propose( + read=read, + kinds=load_kinds(args.kinds), + demo_dir=args.demos, + out=args.out, + actor_params=_parse_actor(args.actor_param), + answers=answers or {}, + environment_id=args.environment_id, + admin_read=admin, + create_views_flag=args.create_views, + reader_account=args.reader_account, + validate=not args.no_validate, + ) + print(f"questions: {len(report['questions'])} ({len(report['questions_open'])} open) -> {args.out / 'QUESTIONS.md'}") + for kind, rep in report["kinds"].items(): + status = "; ".join(rep["refused"]) if rep["refused"] else f"{rep['emitted']} contract(s)" + print(f" {kind}: {status}") + return 0 + except (GenerateError, store.StoreError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py new file mode 100644 index 0000000000..1e5b566bc1 --- /dev/null +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -0,0 +1,335 @@ +"""The contract generator on a real PostgreSQL: demonstrations in, contracts out. + +A throwaway PostgreSQL (``initdb``) holds a small issue tracker shaped like +Redmine: names behind keys with no declared foreign keys, a person's name in +two columns, notes in a journal table, custom fields in an +entity-attribute-value table, look-alike projects with the same issue +subject. The test records two demonstration writes per kind, lets the +generator propose contracts, then runs correct writes and planted faults +through ``begin_write`` and ``verify_write``. Every correct write must be +CONFIRMED and every fault stopped. Skipped when PostgreSQL isn't installed. +Records are made up. +""" + +from __future__ import annotations + +import json +import os +import shutil +import socket +import subprocess +import tempfile +import time +from pathlib import Path +from typing import Any, Callable + +import pytest +import yaml + +psycopg2 = pytest.importorskip("psycopg2") + +from openadapt_flow.generate import verify_contract as gen # noqa: E402 + +INITDB = shutil.which("initdb") or "/opt/homebrew/bin/initdb" +PG_CTL = shutil.which("pg_ctl") or "/opt/homebrew/bin/pg_ctl" + +SCHEMA = """ +CREATE TABLE projects (id serial PRIMARY KEY, name text NOT NULL); +CREATE TABLE users (id serial PRIMARY KEY, login text, firstname text, lastname text); +CREATE TABLE statuses (id serial PRIMARY KEY, name text); +CREATE TABLE issues ( + id serial PRIMARY KEY, project_id integer NOT NULL, subject text NOT NULL, + status_id integer NOT NULL, assigned_to_id integer, due_date date, + done_ratio integer NOT NULL DEFAULT 0, is_private boolean NOT NULL DEFAULT false, + author_id integer NOT NULL, created_on timestamp, updated_on timestamp, + lock_version integer NOT NULL DEFAULT 0); +CREATE TABLE journals (id serial PRIMARY KEY, journalized_id integer, + journalized_type text, user_id integer, notes text DEFAULT '', created_on timestamp); +CREATE TABLE journal_details (id serial PRIMARY KEY, journal_id integer, + prop_key text, old_value text, value text); +CREATE TABLE custom_values (id serial PRIMARY KEY, customized_type text, + customized_id integer, custom_field_id integer, value text); +CREATE TABLE settings (id serial PRIMARY KEY, name text, value text); +INSERT INTO projects (name) VALUES ('Billing Portal'), ('Billing Portal Legacy'), ('Help Desk'); +INSERT INTO users (login, firstname, lastname) VALUES ('bot', 'Ops', 'Bot'), + ('jpark', 'Jon', 'Park'), ('roconnor', 'Renée', 'O''Connor'), ('roconner', 'Renee', 'O''Conner'); +INSERT INTO statuses (name) VALUES ('New'), ('In Progress'), ('Resolved'); +INSERT INTO issues (project_id, subject, status_id, author_id, created_on, updated_on) VALUES + (1, 'Refund button missing', 1, 2, '2026-01-01', '2026-01-01'), + (2, 'Refund button missing', 1, 2, '2026-01-01', '2026-01-01'), + (3, 'Printer jams', 1, 2, '2026-01-01', '2026-01-01'), + (3, 'VPN drops', 2, 2, '2026-01-01', '2026-01-01'); +INSERT INTO settings (name, value) VALUES ('ui_theme', 'classic'); +CREATE ROLE oa_reader LOGIN PASSWORD 'reader-pw'; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO oa_reader; +""" + +BOT = 1 + + +def _free_port() -> int: + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + return int(s.getsockname()[1]) + + +@pytest.fixture(scope="module") +def pg() -> Any: + if not (os.path.exists(INITDB) and os.path.exists(PG_CTL)): + pytest.skip("PostgreSQL (initdb, pg_ctl) is not installed") + root = Path(tempfile.mkdtemp(prefix="oa-gen-pg-")) + port = _free_port() + env = {**os.environ, "LC_ALL": "C"} + subprocess.run([INITDB, "-D", str(root / "data"), "-U", "admin", "--auth=trust", "-E", "UTF8", "--locale=C"], + check=True, capture_output=True, env=env) # fmt: skip + subprocess.run( + [PG_CTL, "-D", str(root / "data"), "-l", str(root / "log"), "-w", "start", "-o", + f"-p {port} -c unix_socket_directories='' -c listen_addresses=127.0.0.1"], + check=True, capture_output=True, env=env, + ) # fmt: skip + try: + yield {"root": root, "port": port} + finally: + subprocess.run([PG_CTL, "-D", str(root / "data"), "-m", "immediate", "stop"], + capture_output=True, env=env) # fmt: skip + shutil.rmtree(root, ignore_errors=True) + + +class App: + """The tracker's own writes, as its API would make them.""" + + def __init__(self, port: int, db: str) -> None: + self.port, self.db = port, db + + def con(self) -> Any: + c = psycopg2.connect(host="127.0.0.1", port=self.port, user="admin", dbname=self.db) + c.autocommit = True + return c + + def q(self, sql: str, args: tuple = ()) -> list[tuple]: + with self.con() as c, c.cursor() as cur: + cur.execute(sql, args) + return cur.fetchall() if cur.description else [] + + def one(self, sql: str, args: tuple = ()) -> Any: + rows = self.q(sql, args) + assert len(rows) == 1, (sql, rows) + return rows[0][0] + + def user(self, name: str) -> int: + first, last = name.split(" ", 1) + return self.one("SELECT id FROM users WHERE firstname=%s AND lastname=%s", (first, last)) + + def create_issue(self, v: dict[str, Any], *, project_id: Any = None) -> int: + pid = project_id or self.one("SELECT id FROM projects WHERE name=%s", (v["project"],)) + sid = self.one("SELECT id FROM statuses WHERE name=%s", (v["status"],)) + uid = self.user(v["assignee"]) if v.get("assignee") else None + iid = self.one( + "INSERT INTO issues (project_id, subject, status_id, assigned_to_id, due_date, " + "author_id, created_on, updated_on) VALUES (%s,%s,%s,%s,%s,%s,now(),now()) RETURNING id", + (pid, v["subject"], sid, uid, v.get("due_date"), BOT), + ) + if v.get("customer_ref") is not None: + self.q("INSERT INTO custom_values (customized_type, customized_id, custom_field_id, value) " + "VALUES ('Issue', %s, 1, %s)", (iid, v["customer_ref"])) # fmt: skip + return iid + + def find(self, project: str, subject: str) -> int: + return self.one( + "SELECT i.id FROM issues i JOIN projects p ON p.id=i.project_id " + "WHERE p.name=%s AND i.subject=%s", (project, subject)) # fmt: skip + + def update_issue(self, iid: int, v: dict[str, Any], *, user: int = BOT) -> None: + sets, details = [], [] + if v.get("status") is not None: + sets.append(("status_id", self.one("SELECT id FROM statuses WHERE name=%s", (v["status"],)))) + if v.get("assignee") is not None: + sets.append(("assigned_to_id", self.user(v["assignee"]))) + if v.get("due_date") is not None: + sets.append(("due_date", v["due_date"])) + if v.get("done_ratio") is not None: + sets.append(("done_ratio", v["done_ratio"])) + for col, val in sets: + old = self.one(f"SELECT {col}::text FROM issues WHERE id=%s", (iid,)) + self.q(f"UPDATE issues SET {col}=%s WHERE id=%s", (val, iid)) + details.append((col, old, str(val))) + self.q("UPDATE issues SET updated_on=now(), lock_version=lock_version+1 WHERE id=%s", (iid,)) + jid = self.one( + "INSERT INTO journals (journalized_id, journalized_type, user_id, notes, created_on) " + "VALUES (%s,'Issue',%s,%s,now()) RETURNING id", (iid, user, v.get("notes") or "")) # fmt: skip + for col, old, new in details: + self.q("INSERT INTO journal_details (journal_id, prop_key, old_value, value) " + "VALUES (%s,%s,%s,%s)", (jid, col, old, new)) # fmt: skip + + +KINDS = { + "kinds": [ + { + "kind": "create_issue", + "parameters": ["project", "subject", "status", "assignee", "due_date", "customer_ref"], + "examples": [ + {"values": {"project": "Billing Portal", "subject": "Tax rounding", "status": "New", + "assignee": "Jon Park", "due_date": "2026-11-20", "customer_ref": None}}, + {"values": {"project": "Help Desk", "subject": "Onboard Zoë", "status": "New", + "assignee": "Renée O'Connor", "due_date": None, "customer_ref": "007341"}}, + ], + }, + { + "kind": "update_issue", + "parameters": ["project", "subject", "status", "assignee", "due_date", "done_ratio", "notes"], + "examples": [ + {"values": {"project": "Help Desk", "subject": "Printer jams", "status": "In Progress", + "assignee": "Jon Park", "due_date": None, "done_ratio": None, "notes": None}}, + {"values": {"project": "Billing Portal", "subject": "Refund button missing", + "status": None, "assignee": None, "due_date": None, "done_ratio": 30, + "notes": "Customer confirmed the refund."}}, + ], + }, + ] +} + + +@pytest.fixture(scope="module") +def generated(pg: Any) -> dict[str, Any]: + root, port = pg["root"], pg["port"] + admin = psycopg2.connect(host="127.0.0.1", port=port, user="admin", dbname="postgres") + admin.autocommit = True + admin.cursor().execute("CREATE DATABASE tracker") + admin.close() + app = App(port, "tracker") + with app.con() as c, c.cursor() as cur: + cur.execute(SCHEMA) + os.environ["OA_GEN_TEST_PW"] = "reader-pw" + work = root / "work" + work.mkdir() + read = {"sql_driver": "psycopg2", + "sql_connect_args": {"host": "127.0.0.1", "port": port, "user": "oa_reader", "dbname": "tracker"}, + "sql_password_env": "OA_GEN_TEST_PW"} # fmt: skip + (work / "read.yaml").write_text(yaml.safe_dump(read)) + (work / "kinds.yaml").write_text(yaml.safe_dump(KINDS, allow_unicode=True)) + common = ["--read", str(work / "read.yaml")] + assert gen.main(["schema", *common]) == 0 + + def demo(kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: + assert gen.main(["snapshot", *common, "--out", str(work / "before.json")]) == 0 + write() + assert gen.main(["record", *common, "--before", str(work / "before.json"), "--kind", kind, + "--values", json.dumps(values), "--demos", str(work / "demos")]) == 0 # fmt: skip + + for ex in KINDS["kinds"][0]["examples"]: + demo("create_issue", ex["values"], lambda v=ex["values"]: app.create_issue(v)) + for ex in KINDS["kinds"][1]["examples"]: + v = ex["values"] + demo("update_issue", v, lambda v=v: app.update_issue(app.find(v["project"], v["subject"]), v)) + argv = ["propose", *common, "--kinds", str(work / "kinds.yaml"), "--demos", str(work / "demos"), + "--out", str(work / "out"), "--actor-param", f"bot_user_id={BOT}"] # fmt: skip + assert gen.main(argv) == 0 + report = json.loads((work / "out" / "report.json").read_text()) + # The person accepts the proposals. + answers = {q["key"]: q["proposal"] for q in report["questions"]} + (work / "answers.yaml").write_text(yaml.safe_dump(answers)) + assert gen.main([*argv, "--answers", str(work / "answers.yaml")]) == 0 + if os.environ.get("OA_GEN_TEST_KEEP"): + shutil.copytree(work, os.environ["OA_GEN_TEST_KEEP"], dirs_exist_ok=True) + return {"app": app, "out": work / "out", "report": report, "read": read} + + +def _run(generated: dict[str, Any], kind: str, values: dict[str, Any], write: Callable[[], Any]) -> str: + from openadapt_flow.verifier import Actor, begin_write, verify_write + + contract, params = gen.choose(generated["out"] / "manifest.yaml", kind, values, {"bot_user_id": str(BOT)}) + with tempfile.TemporaryDirectory() as tmp: + pending = begin_write(contract, params, actor=Actor("agent", "ops-bot"), ledger=None, + write_key=f"t-{time.time_ns()}", workdir=tmp) # fmt: skip + write() + result = verify_write(pending) + return getattr(result.outcome, "value", str(result.outcome)) + " " + "; ".join(result.reasons)[:400] + + +def test_questions_and_contracts(generated: dict[str, Any]) -> None: + report = generated["report"] + keys = [q["key"] for q in report["questions"]] + assert len(keys) <= 3, keys + kinds = report["kinds"] + assert kinds["create_issue"]["mode"] == "create" + assert kinds["update_issue"]["mode"] == "update" + assert kinds["create_issue"]["emitted"] > 0, kinds["create_issue"] + # Four change fields the examples give (status, assignee, done ratio, + # notes): every non-empty subset gets a contract. No example gives a due + # date, so no contract changes one. + assert kinds["update_issue"]["emitted"] == 15, json.dumps(kinds["update_issue"], indent=1) + assert "journal_details" in kinds["update_issue"]["logs"] + + +def test_a_correct_create_is_confirmed(generated: dict[str, Any]) -> None: + app = generated["app"] + v = {"project": "Billing Portal", "subject": "Dark mode", "status": "New", + "assignee": "Renée O'Connor", "due_date": "2026-12-01", "customer_ref": "0099"} # fmt: skip + assert _run(generated, "create_issue", v, lambda: app.create_issue(v)).startswith("CONFIRMED") + + +def test_found_existing_with_no_write_is_confirmed(generated: dict[str, Any]) -> None: + app = generated["app"] + v = {"project": "Help Desk", "subject": "Make sure", "status": "New", + "assignee": "Jon Park", "due_date": None, "customer_ref": None} # fmt: skip + app.create_issue(v) + assert _run(generated, "create_issue", v, lambda: None).startswith("CONFIRMED") + + +def test_an_undemonstrated_update_combination_is_confirmed(generated: dict[str, Any]) -> None: + app = generated["app"] + v = {"project": "Help Desk", "subject": "VPN drops", "status": "Resolved", "assignee": None, + "due_date": None, "done_ratio": 80, "notes": "Fixed the tunnel."} # fmt: skip + iid = app.find("Help Desk", "VPN drops") + assert _run(generated, "update_issue", v, lambda: app.update_issue(iid, v)).startswith("CONFIRMED") + + +CREATE = {"project": "Billing Portal", "subject": "Export CSV", "status": "New", + "assignee": "Jon Park", "due_date": "2026-12-05", "customer_ref": "0042"} # fmt: skip +UPDATE = {"project": "Billing Portal", "subject": "Refund button missing", "status": "Resolved", + "assignee": None, "due_date": None, "done_ratio": None, "notes": "Done."} # fmt: skip + + +def _faults(app: App) -> dict[str, tuple[str, dict[str, Any], Callable[[], Any]]]: + def legacy() -> int: + return app.find("Billing Portal Legacy", "Refund button missing") + + def target() -> int: + return app.find("Billing Portal", "Refund button missing") + + return { + "wrong value": ("create_issue", CREATE, lambda: app.create_issue({**CREATE, "assignee": "Renee O'Conner"})), + "wrong project": ("create_issue", CREATE, lambda: app.create_issue(CREATE, project_id=2)), + "duplicate": ("create_issue", CREATE, lambda: (app.create_issue(CREATE), app.create_issue(CREATE))), + "nothing written": ("create_issue", CREATE, lambda: None), + "missing custom field": ("create_issue", CREATE, lambda: app.create_issue({**CREATE, "customer_ref": None})), + "extra issue": ("create_issue", CREATE, lambda: (app.create_issue(CREATE), app.create_issue({**CREATE, "subject": "Follow-up"}))), + "setting changed": ("create_issue", CREATE, lambda: (app.create_issue(CREATE), app.q("UPDATE settings SET value='dark'"))), + "look-alike target": ("update_issue", UPDATE, lambda: app.update_issue(legacy(), UPDATE)), + "both targets": ("update_issue", UPDATE, lambda: (app.update_issue(target(), UPDATE), app.update_issue(legacy(), {"status": "Resolved"}))), + "note missing": ("update_issue", UPDATE, lambda: app.update_issue(target(), {**UPDATE, "notes": None})), + "collateral field": ("update_issue", UPDATE, lambda: (app.update_issue(target(), UPDATE), app.q("UPDATE issues SET is_private=true WHERE id=%s", (target(),)))), + "collateral delete": ("update_issue", UPDATE, lambda: (app.update_issue(target(), UPDATE), app.q("DELETE FROM issues WHERE subject='Printer jams'"))), + "note by someone else": ("update_issue", UPDATE, lambda: app.update_issue(target(), UPDATE, user=2)), + } # fmt: skip + + +@pytest.mark.parametrize("name", list(_faults(App(0, "")).keys())) +def test_each_fault_is_stopped(generated: dict[str, Any], name: str) -> None: + app = generated["app"] + kind, values, write = _faults(app)[name] + with app.con() as c, c.cursor() as cur: # each fault starts from the same rows + cur.execute("BEGIN") + snapshot = {t: cur.execute(f"SELECT * FROM {t}") or cur.fetchall() + for t in ("issues", "journals", "journal_details", "custom_values", "settings")} # fmt: skip + cur.execute("COMMIT") + try: + outcome = _run(generated, kind, values, write) + print(f"FAULT {name}: {outcome}") + assert not outcome.startswith("CONFIRMED"), (name, outcome) + finally: + with app.con() as c, c.cursor() as cur: + for t, rows in snapshot.items(): + cur.execute(f"DELETE FROM {t}") + for r in rows: + cur.execute(f"INSERT INTO {t} VALUES ({', '.join(['%s'] * len(r))})", r) From 55adc3fa46e9c7458ac28862d09685a51c0a724c Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:30:30 -0400 Subject: [PATCH 40/94] WIP: generator trigger grant, demo and validation scripts --- .../generate_contracts/demo_dolibarr.py | 97 +++++++++++ .../benchmark/generate_contracts/demo_lib.py | 117 +++++++++++++ .../benchmark/generate_contracts/validate.py | 123 +++++++++++++ .../generate/verify_contract.py | 164 +++++++++++++----- 4 files changed, 458 insertions(+), 43 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/demo_dolibarr.py create mode 100644 packages/flow/benchmark/generate_contracts/demo_lib.py create mode 100644 packages/flow/benchmark/generate_contracts/validate.py diff --git a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py new file mode 100644 index 0000000000..4fd6a683ac --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py @@ -0,0 +1,97 @@ +"""Dolibarr (first study, instance 1): demonstrations, then the generator. + + FCG_INSTANCE=1 python benchmark/generate_contracts/demo_dolibarr.py WORKDIR + +Makes one demonstration write per example in ``setup_view.yaml`` through +Dolibarr's REST API as the bot (an example whose record already exists is +skipped: the right outcome is no write), then runs ``propose``. +""" + +from __future__ import annotations + +import calendar +import sys +from pathlib import Path + +import pymysql +import requests + +from demo_lib import demo, fixture, propose, setup_view, write_inputs + +APP = "dolibarr" + + +def main(work: Path) -> None: + fx = fixture(APP) + fx.reset() + bot = fx.bot() + ro = fx.read_access() + read = { + "sql_driver": "pymysql", + "sql_connect_args": {"host": ro["host"], "port": ro["port"], "user": ro["user"], "database": ro["database"]}, + "sql_password_env": "FCG_DOLIBARR_READER_PASSWORD", + } + write_inputs(work, APP, read) + db = pymysql.connect(host=ro["host"], port=ro["port"], user=ro["user"], password=ro["password"], database=ro["database"]) + + def q(sql: str, args: tuple = ()) -> list[tuple]: + with db.cursor() as cur: + cur.execute(sql, args) + return list(cur.fetchall()) + + def api(method: str, path: str, payload: object = None) -> object: + r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r.raise_for_status() + return r.json() + + def customer(name: str, town: str) -> int | None: + db.commit() + rows = q("SELECT rowid FROM llx_societe WHERE BINARY nom=%s AND BINARY town=%s", (name, town)) + return int(rows[0][0]) if len(rows) == 1 else None + + def country(label: str) -> int: + return int(q("SELECT rowid FROM llx_c_country WHERE label=%s", (label,))[0][0]) + + def product(ref: str) -> tuple[int, str, str]: + db.commit() + rid, price, tva = q("SELECT rowid, price, tva_tx FROM llx_product WHERE BINARY ref=%s", (ref,))[0] + return int(rid), str(price), str(tva) + + for k in setup_view(APP)["kinds"]: + for ex in k["examples"]: + v = ex["values"] + kind = k["kind"] + if kind == "create_customer": + if customer(v["name"], v["town"]) is not None: + continue + payload = {"name": v["name"], "address": v["address"], "zip": v["zip"], "town": v["town"], + "country_id": country(v["country"]), "phone": v["phone"], "email": v["email"], "client": 1} # fmt: skip + demo(work, kind, v, lambda p=payload: api("POST", "/thirdparties", p)) + elif kind == "create_product": + db.commit() + if q("SELECT 1 FROM llx_product WHERE BINARY ref=%s", (v["ref"],)): + continue + payload = {"ref": v["ref"], "label": v["label"], "type": 0, "price": v["price_excl_vat"], + "price_base_type": "HT", "tva_tx": v["vat_rate"], "status": 1, "status_buy": 1} # fmt: skip + demo(work, kind, v, lambda p=payload: api("POST", "/products", p)) + elif kind == "create_draft_invoice": + y, m, d = (int(x) for x in v["invoice_date"].split("-")) + lines = [] + for ln in v["lines"]: + pid, price, tva = product(ln["product_ref"]) + lines.append({"fk_product": pid, "qty": ln["qty"], "subprice": price, "tva_tx": tva, "product_type": 0}) + payload = {"socid": customer(v["customer"], v["town"]), "type": 0, + "date": calendar.timegm((y, m, d, 12, 0, 0, 0, 0, 0)), "lines": lines} # fmt: skip + demo(work, kind, v, lambda p=payload: api("POST", "/invoices", p)) + elif kind == "update_customer": + cid = customer(v["customer"], v["town"]) + change = {key[4:]: x for key, x in v.items() if key.startswith("new_") and x is not None} + demo(work, kind, v, lambda c=cid, p=change: api("PUT", f"/thirdparties/{c}", p)) + bot_id = q("SELECT rowid FROM llx_user WHERE login='fcg_bot'")[0][0] + report = propose(work, {"bot_user_id": str(bot_id)}) + print("questions:", len(report["questions"])) + fx.reset() + + +if __name__ == "__main__": + main(Path(sys.argv[1])) diff --git a/packages/flow/benchmark/generate_contracts/demo_lib.py b/packages/flow/benchmark/generate_contracts/demo_lib.py new file mode 100644 index 0000000000..f0f82bf224 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/demo_lib.py @@ -0,0 +1,117 @@ +"""Shared steps for running the contract generator on a first-study app. + +The generator is run the way a new customer would run it: it gets only the +app's read-only database account, the kinds and parameter names from the +app's ``setup_view.yaml``, and demonstration writes made here through the +app's public REST API (one per example). Nothing from the study's tasks, +oracle or faults reaches the generator. ``validate.py`` uses those later, +only to judge the contracts it wrote. + +Paths: ``FCG`` is the first study's folder (``apps//fixture.py``). +""" + +from __future__ import annotations + +import importlib.util +import json +import os +import sys +from pathlib import Path +from typing import Any, Callable + +import yaml + +from openadapt_flow.generate import verify_contract as gen + +FCG = Path( + os.environ.get( + "FCG_DIR", + str( + Path.home() + / "oa/worktrees/fcg-retest/evidence/first-contact-generalization-2026-10" + ), + ) +) + + +def load(name: str, path: Path) -> Any: + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type] + sys.modules[name] = mod + spec.loader.exec_module(mod) # type: ignore[union-attr] + return mod + + +def fixture(app: str) -> Any: + sys.path.insert(0, str(FCG / "apps" / app)) + return load(f"fcg_fixture_{app}", FCG / "apps" / app / "fixture.py") + + +def setup_view(app: str) -> dict[str, Any]: + return yaml.safe_load((FCG / "apps" / app / "setup_view.yaml").read_text(encoding="utf-8")) + + +def write_inputs(work: Path, app: str, read: dict[str, Any]) -> list[str]: + """``read.yaml`` and ``kinds.yaml`` in ``work``; the CLI's read flags.""" + + work.mkdir(parents=True, exist_ok=True) + (work / "read.yaml").write_text(yaml.safe_dump(read), encoding="utf-8") + view = setup_view(app) + kinds = { + "kinds": [ + { + "kind": k["kind"], + "description": k.get("description", ""), + "parameters": k["parameters"], + "examples": [{"values": e["values"]} for e in k.get("examples", [])], + } + for k in view["kinds"] + ] + } + (work / "kinds.yaml").write_text(yaml.safe_dump(kinds, allow_unicode=True), encoding="utf-8") + return ["--read", str(work / "read.yaml")] + + +def demo(work: Path, kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: + """Snapshot, make one demonstration write, record the diff.""" + + flags = ["--read", str(work / "read.yaml")] + assert gen.main(["snapshot", *flags, "--out", str(work / "before.json")]) == 0 + write() + assert ( + gen.main( + [ + "record", + *flags, + "--before", + str(work / "before.json"), + "--kind", + kind, + "--values", + json.dumps(values, ensure_ascii=False), + "--demos", + str(work / "demos"), + ] + ) + == 0 + ) + + +def propose(work: Path, actor: dict[str, str], extra: list[str] = ()) -> dict[str, Any]: + argv = [ + "propose", + "--read", + str(work / "read.yaml"), + "--kinds", + str(work / "kinds.yaml"), + "--demos", + str(work / "demos"), + "--out", + str(work / "out"), + *[f"--actor-param={k}={v}" for k, v in actor.items()], + *extra, + ] + if (work / "answers.yaml").exists(): + argv += ["--answers", str(work / "answers.yaml")] + gen.main(argv) + return json.loads((work / "out" / "report.json").read_text(encoding="utf-8")) diff --git a/packages/flow/benchmark/generate_contracts/validate.py b/packages/flow/benchmark/generate_contracts/validate.py new file mode 100644 index 0000000000..65f7eedab3 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/validate.py @@ -0,0 +1,123 @@ +"""Judge generated contracts on a first-study app: correct writes and faults. + + FCG_INSTANCE=1 python benchmark/generate_contracts/validate.py dolibarr WORKDIR [--only T01 F05] + +For every task of the study, the pack's reference correct write; for every +planted fault, the fault. Each case: reset the app, pick the contract with +``verify_contract.choose`` (from the task's values only), ``begin_write``, +write, ``verify_write``, and the study's oracle labels what happened. Results +append to ``WORKDIR/validate.jsonl``; a case already there is skipped. + +The tasks, faults and oracle are read here only, never by the generator. +""" + +from __future__ import annotations + +import argparse +import json +import os +import tempfile +import time +import traceback +from pathlib import Path +from typing import Any + +import yaml + +from demo_lib import FCG, fixture, load + +from openadapt_flow.generate import verify_contract as gen +from openadapt_flow.verifier import Actor, begin_write, verify_write + +LATE_WAIT_S = 150 + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("app") + ap.add_argument("work", type=Path) + ap.add_argument("--only", nargs="*") + ap.add_argument("--actor-id", required=True, help="the bot's id as the contracts bind it") + ap.add_argument("--actor-param", default="bot_user_id") + ap.add_argument("--no-faults", action="store_true") + ap.add_argument("--no-correct", action="store_true") + args = ap.parse_args() + os.environ.setdefault("OPENADAPT_LOCAL_SEAL", "off") + app_dir = FCG / "apps" / args.app + fx = fixture(args.app) + oracle = load(f"fcg_oracle_{args.app}", app_dir / "oracle.py") + faults = load(f"fcg_faults_{args.app}", app_dir / "faults" / "faults.py") + tasks = {t["id"]: t for t in yaml.safe_load((app_dir / "tasks.yaml").read_text())} + manifest = args.work / "out" / "manifest.yaml" + out = args.work / "validate.jsonl" + done = set() + if out.exists(): + done = {json.loads(x)["case"] for x in out.read_text().splitlines() if x.strip()} + cases: list[tuple[str, dict, Any, float]] = [] + if not args.no_correct: + for tid, task in tasks.items(): + cases.append((f"correct-{tid}", task, lambda t=task: faults.correct_write(t, fx.bot()), 0.0)) + if not args.no_faults: + for f in yaml.safe_load((app_dir / "faults" / "faults.yaml").read_text()): + task = tasks[f["task"]] + fn = getattr(faults, f["id"]) + late = 0.0 + if f.get("class") == "late_write" or f.get("after_snapshot_delay_s"): + late = max(LATE_WAIT_S, float(f.get("after_snapshot_delay_s") or 0)) + cases.append( + (f"fault-{f['id']}", task, + lambda fn=fn, t=task: fn(t, fx.bot(), fx.other_user(), fx.read_access()), late) + ) # fmt: skip + for case, task, act, late in cases: + short = case.split("-", 1)[1] + if case in done or (args.only and short not in args.only and task["id"] not in args.only): + continue + while os.getloadavg()[0] >= 35: + time.sleep(30) + rec: dict[str, Any] = {"case": case, "task": task["id"], "kind": task["kind"]} + fx.reset() + before = oracle.snapshot() + pending = None + try: + contract, params = gen.choose(manifest, task["kind"], task["values"], {args.actor_param: args.actor_id}) + rec["contract"] = contract.name + tmp = tempfile.mkdtemp() + pending = begin_write(contract, params, actor=Actor("agent", "generated-check"), + ledger=None, write_key=f"{case}-{time.time_ns()}", workdir=tmp) # fmt: skip + except Exception as exc: # noqa: BLE001 - a stop before the write + rec["verdict"] = {"outcome": "BLOCKED_AT_BEGIN", "reasons": [f"{type(exc).__name__}: {exc}"]} + try: + rec["claim"] = act() + except Exception as exc: # noqa: BLE001 + rec["claim"] = {"status": "not_done", "note": f"{type(exc).__name__}: {exc}"} + if pending is not None: + try: + r = verify_write(pending) + rec["verdict"] = { + "outcome": getattr(r.outcome, "value", str(r.outcome)), + "reasons": list(r.reasons), + "recheck_due_at": getattr(r, "recheck_due_at", None), + } + except Exception as exc: # noqa: BLE001 + rec["verdict"] = {"outcome": "VERIFIER_CRASH", "reasons": [f"{type(exc).__name__}: {exc}"], + "trace": traceback.format_exc()[-2000:]} # fmt: skip + if late: + time.sleep(late) + if hasattr(fx, "wait_late_writes"): + fx.wait_late_writes() + after = oracle.snapshot() + try: + rec["oracle"] = oracle.judge(task, before, after) + except Exception as exc: # noqa: BLE001 + rec["oracle"] = {"label": "oracle_error", "reasons": [str(exc)]} + with out.open("a", encoding="utf-8") as f: + f.write(json.dumps(rec, default=str, ensure_ascii=False) + "\n") + v = rec["verdict"] + print(f"{case} [{task['kind']}]: {v['outcome']} | oracle {rec['oracle'].get('label')} | " + f"{'; '.join(v.get('reasons', []))[:200]}", flush=True) # fmt: skip + fx.reset() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index c897bf84ca..caccdbf34a 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1307,7 +1307,27 @@ def dry_read(path: Path, params: Mapping[str, Any], mysql: bool) -> Optional[str if mysql: snap = detail.get(FRAME_DB_DETAIL) if not isinstance(snap, dict): - return "dry read: the whole-database frame could not be read" + from openadapt_flow.runtime.effects.db_snapshot import mac_key, take_snapshot + from openadapt_flow.verify_run import _audit_sql, _db_connect, _ref_columns + + database = contract.config.frame.database + try: + take_snapshot( + _db_connect(database.read), + database.dialect, + key=mac_key(b"0" * 32), + modes=database.modes(), + max_rows=database.max_rows, + extra_queries=_audit_sql(contract.config), + ignore=database.all_ignore_columns(), + ref_columns=_ref_columns(contract.config), + schemas=database.schemas, + links=database.recheck_after_s is not None, + ) + why = "no error on a second read" + except Exception as exc: # noqa: BLE001 - reported + why = f"{type(exc).__name__}: {exc}" + return f"dry read: the whole-database frame could not be read ({why})" if snap.get("code_visible") is False: return ( "dry read: the read account can't see triggers (MySQL needs the " @@ -1375,13 +1395,7 @@ def guard_problem(sql: str) -> Optional[str]: return None -def create_views( - admin_read: Mapping[str, Any], schema: Schema, tables: Sequence[str], reader: str -) -> dict[str, str]: - """Create one read-only view per table the SQL guard refuses by name, - in the same database, and grant the read account SELECT on it. Only run - with ``--create-views``; the connection is the admin one given.""" - +def _admin_connect(admin_read: Mapping[str, Any]) -> Any: import importlib module = importlib.import_module(str(admin_read["sql_driver"])) @@ -1391,7 +1405,51 @@ def create_views( if env not in os.environ: raise GenerateError(f"environment variable {env} (the admin password) is not set") args["password"] = os.environ[str(env)] - conn = module.connect(**args) + return module.connect(**args) + + +def grant_trigger(admin_read: Mapping[str, Any], schema: Schema, reader: str) -> str: + """MySQL shows a table's triggers only to an account with the TRIGGER + privilege, and the whole-database frame must see them (a trigger planted + during a write could otherwise pass). Run only with ``--grant-trigger``.""" + + conn = _admin_connect(admin_read) + try: + sql = f"GRANT TRIGGER ON {store.quote('mysql', schema.database)}.* TO {reader}" + conn.cursor().execute(sql) + conn.commit() + finally: + conn.close() + return sql + + +def _sees_triggers(connect: Any) -> bool: + """Does the account hold TRIGGER (or ALL) on the database? A database + with no triggers shows none either way, so the grant is what's checked.""" + + conn = connect() + try: + cur = conn.cursor() + cur.execute("SHOW GRANTS") + grants = [str(g[0]).upper() for g in cur.fetchall()] + finally: + conn.rollback() + conn.close() + for g in grants: + privs = g.split(" ON ")[0] + if "TRIGGER" in privs or "ALL PRIVILEGES" in privs: + return True + return False + + +def create_views( + admin_read: Mapping[str, Any], schema: Schema, tables: Sequence[str], reader: str +) -> dict[str, str]: + """Create one read-only view per table the SQL guard refuses by name, + in the same database, and grant the read account SELECT on it. Only run + with ``--create-views``; the connection is the admin one given.""" + + conn = _admin_connect(admin_read) made = {} try: cur = conn.cursor() @@ -1427,6 +1485,7 @@ def propose( create_views_flag: bool = False, reader_account: str = "", validate: bool = True, + grant_trigger_flag: bool = False, ) -> dict[str, Any]: """Analyze, ask, emit and validate. Returns the report (also written).""" @@ -1439,6 +1498,20 @@ def propose( "the read account is not read-only, so no contract is proposed: " + "; ".join(problems) ) + applied: list[str] = [] + if dialect == "mysql" and not _sees_triggers(connect): + if grant_trigger_flag: + if not admin_read or not reader_account: + raise GenerateError("--grant-trigger needs --admin-read and --reader-account") + applied.append(grant_trigger(admin_read, schema, reader_account)) + if not _sees_triggers(connect): + raise GenerateError( + "the read account can't see triggers, so the whole-database frame " + "would never be checked (every write would be UNCERTAIN). MySQL shows " + "triggers only to an account with the TRIGGER privilege. Rerun with " + "--grant-trigger --admin-read --reader-account , " + "or have the DBA run: GRANT TRIGGER ON .* TO " + ) demos = load_demos(demo_dir) env = environment_id or f"generated-{schema.database}" env = re.sub(r"[^A-Za-z0-9._:-]", "-", env) @@ -1447,6 +1520,41 @@ def propose( plans = [analyze(schema, k, demos, actor_params) for k in kinds] questions: list[Question] = [] + location_questions: list[Question] = [] + # Q: per kind, a value the generator couldn't place. + for p in plans: + if not p.unresolved: + continue + chosen = dict(_answer(answers, f"locations.{p.kind}") or {}) + open_ = {} + for name, tied in p.unresolved.items(): + pick = chosen.get(name) + if isinstance(pick, int) and 0 <= pick < len(tied): + p.locations[name] = tied[pick] + if p.mode == "update" and name not in p.identity + p.changes: + p.changes.append(name) + else: + open_[name] = tied + p.unresolved = open_ + if open_: + parts = [] + for name, tied in open_.items(): + if tied: + opts = "; ".join(f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied)) + parts.append(f"`{name}` is in several places: {opts}") + else: + parts.append( + f"`{name}` was not found in any row the demonstrations wrote; " + "tasks that give it are stopped" + ) + location_questions.append( + Question( + f"locations.{p.kind}", + f"Kind `{p.kind}`: " + " ".join(parts) + + f" Answer `locations.{p.kind}: {{: }}` to pick one.", + None, + ) + ) # Q: bookkeeping, for the whole app at once. logs = set().union(*(p.logs for p in plans)) if plans else set() book: dict[str, set[str]] = {} @@ -1512,40 +1620,7 @@ def propose( DEFAULT_RECHECK_S, ) ) - # Q: per kind, a value the generator couldn't place. - for p in plans: - if not p.unresolved: - continue - chosen = dict(_answer(answers, f"locations.{p.kind}") or {}) - open_ = {} - for name, tied in p.unresolved.items(): - pick = chosen.get(name) - if isinstance(pick, int) and 0 <= pick < len(tied): - p.locations[name] = tied[pick] - if p.mode == "update" and name not in p.identity + p.changes: - p.changes.append(name) - else: - open_[name] = tied - p.unresolved = open_ - if open_: - parts = [] - for name, tied in open_.items(): - if tied: - opts = "; ".join(f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied)) - parts.append(f"`{name}` is in several places: {opts}") - else: - parts.append( - f"`{name}` was not found in any row the demonstrations wrote; " - "tasks that give it are stopped" - ) - questions.append( - Question( - f"locations.{p.kind}", - f"Kind `{p.kind}`: " + " ".join(parts) - + f" Answer `locations.{p.kind}: {{: }}` to pick one.", - None, - ) - ) + questions.extend(location_questions) questions_open = [ q for q in questions if _answer(answers, q.key) is None ] @@ -1593,6 +1668,7 @@ def propose( "kinds": {}, } report: dict[str, Any] = { + "applied": applied, "dialect": dialect, "tables": len(schema.tables), "questions": [ @@ -1832,6 +1908,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: s.add_argument("--create-views", action="store_true", help="create a view for a table the SQL guard refuses by name (needs --admin-read)") s.add_argument("--admin-read", type=Path, help="admin.yaml: a connection allowed to CREATE VIEW and GRANT") s.add_argument("--reader-account", default="", help="the read account to GRANT SELECT on created views (MySQL: 'user'@'%%')") + s.add_argument("--grant-trigger", action="store_true", help="MySQL: GRANT TRIGGER to the read account through --admin-read, so the frame can see triggers") s.add_argument("--no-validate", action="store_true", help="skip the dry read (tests only)") args = ap.parse_args(argv) try: @@ -1897,6 +1974,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: create_views_flag=args.create_views, reader_account=args.reader_account, validate=not args.no_validate, + grant_trigger_flag=args.grant_trigger, ) print(f"questions: {len(report['questions'])} ({len(report['questions_open'])} open) -> {args.out / 'QUESTIONS.md'}") for kind, rep in report["kinds"].items(): From 07faa683f4feeb700a68c0a87e7c86139d8fd0fc Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:33:29 -0400 Subject: [PATCH 41/94] WIP: by-name update fields; keep answered questions counted --- .../generate/verify_contract.py | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index caccdbf34a..94d9aabfad 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -224,6 +224,8 @@ class KindPlan: unresolved: dict[str, list[Location]] = field(default_factory=dict) refused: list[str] = field(default_factory=list) removed: dict[str, int] = field(default_factory=dict) + #: Parameters placed by name, not by a demonstration. + by_name: list[str] = field(default_factory=list) def _scalars(values: Mapping[str, Any]) -> dict[str, Any]: @@ -341,6 +343,12 @@ def analyze( loc, tied = best(_intersect(found[p])) if loc is None: if not found[p]: + named = _by_name(schema, plan, p) + if named is not None: + plan.locations[p] = named + plan.by_name.append(p) + if plan.mode == "update": + plan.changes.append(p) continue # never given: no contract uses it plan.unresolved[p] = tied continue @@ -378,6 +386,27 @@ def analyze( return plan +def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: + """An update parameter no demonstration gives (``new_zip``), placed at + the main table's column of the same name after a leading ``new_`` or + ``set_`` (``zip``), when one exists. Compared exactly by the column's + type. Reported as placed by name; a demonstration that gives it replaces + this.""" + + if plan.mode != "update" or not plan.anchor: + return None + name = re.sub(r"^(new|set)_", "", param) + if name == param: + return None + col = schema.tables[plan.anchor].column(name) + if col is None or name in schema.tables[plan.anchor].primary_key: + return None + rule = {"number": "number", "bool": "number", "date": "date", "datetime": "datetime"}.get(col.kind, "exact") + if col.kind not in ("text", "number", "bool", "date", "datetime"): + return None + return Location((), (name,), rule) + + def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: """Group child-row locations, and find each list parameter's rows.""" @@ -1535,10 +1564,11 @@ def propose( p.changes.append(name) else: open_[name] = tied + asked = dict(p.unresolved) p.unresolved = open_ - if open_: + if asked: parts = [] - for name, tied in open_.items(): + for name, tied in asked.items(): if tied: opts = "; ".join(f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied)) parts.append(f"`{name}` is in several places: {opts}") @@ -1555,6 +1585,8 @@ def propose( None, ) ) + if not open_: + answers = {**answers, f"locations.{p.kind}": chosen} # Q: bookkeeping, for the whole app at once. logs = set().union(*(p.logs for p in plans)) if plans else set() book: dict[str, set[str]] = {} @@ -1691,6 +1723,7 @@ def propose( "mode": p.mode, "main_table": p.anchor, "locations": {k: v.describe(p.anchor) for k, v in sorted(p.locations.items())}, + "placed_by_name": sorted(p.by_name), "children": {k: g.table for k, g in p.children.items()}, "lists": {k: g.table for k, g in p.lists.items()}, "side_rows": [{"table": s.table, "count": s.count, "unless": s.unless} for s in p.sides], From c149d37ec42cf38c957e78bf2c2a9230b01d51fe Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:34:36 -0400 Subject: [PATCH 42/94] WIP: copied-column name preference --- packages/flow/openadapt_flow/generate/demo_analysis.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 404033242f..e51a5e8a02 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -619,6 +619,15 @@ def _copied( candidates = here if candidates is None else candidates & here if not candidates: return None + if candidates and len(candidates) > 1: + # Several columns of the row it points to hold the same value: keep + # the one named like this column (``tva_tx`` from ``tva_tx``, then + # ``subprice`` from ``price``), if exactly one is. + for test in (lambda c: c[3] == name, lambda c: c[3] in name): + named = {c for c in candidates if test(c)} + if len(named) == 1: + candidates = named + break if candidates and len(candidates) == 1: via, target, ref, source = next(iter(candidates)) return {"via": via, "table": target, "ref_column": ref, "column": source} From a4a63fae2c17f0f132f51c915ef3cb705c1a8325 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:35:48 -0400 Subject: [PATCH 43/94] WIP: copied column prefers the table its key names --- packages/flow/openadapt_flow/generate/demo_analysis.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index e51a5e8a02..16f0a543b9 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -623,7 +623,14 @@ def _copied( # Several columns of the row it points to hold the same value: keep # the one named like this column (``tva_tx`` from ``tva_tx``, then # ``subprice`` from ``price``), if exactly one is. - for test in (lambda c: c[3] == name, lambda c: c[3] in name): + def stem(col: str) -> str: + return re.sub(r"(^fk_|_id$|_fk$|^id_)", "", col.lower()) + + for test in ( + lambda c: c[3] == name, + lambda c: c[3] in name, + lambda c: c[3] in name and c[1].lower().endswith(stem(c[0])), + ): named = {c for c in candidates if test(c)} if len(named) == 1: candidates = named From 3ee84dd15da43e51d70be5195eeec6069a140613 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:42:30 -0400 Subject: [PATCH 44/94] WIP: compare number and date columns by value; docs --- .../generate_contracts/demo_redmine.py | 122 ++++++++++ packages/flow/docs/GENERATE_CONTRACTS.md | 216 ++++++++++++++++++ .../openadapt_flow/generate/demo_analysis.py | 5 +- .../generate/ensure_contract.py | 7 +- .../generate/verify_contract.py | 2 +- 5 files changed, 349 insertions(+), 3 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/demo_redmine.py create mode 100644 packages/flow/docs/GENERATE_CONTRACTS.md diff --git a/packages/flow/benchmark/generate_contracts/demo_redmine.py b/packages/flow/benchmark/generate_contracts/demo_redmine.py new file mode 100644 index 0000000000..1135204fec --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/demo_redmine.py @@ -0,0 +1,122 @@ +"""Redmine (first study, instance 1): demonstrations, then the generator. + + FCG_INSTANCE=1 python benchmark/generate_contracts/demo_redmine.py WORKDIR + +One demonstration write per example in ``setup_view.yaml``, through +Redmine's REST API as the bot. Ids for names (project, tracker, user) are +looked up through the read-only account, as a setup agent would. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import psycopg2 +import requests + +from demo_lib import demo, fixture, propose, setup_view, write_inputs + +APP = "redmine" + + +def main(work: Path) -> None: + fx = fixture(APP) + fx.reset() + bot = fx.bot() + ro = fx.read_access() + read = { + "sql_driver": "psycopg2", + "sql_connect_args": {"host": ro["host"], "port": ro["port"], "user": ro["user"], "dbname": ro["database"]}, + "sql_password_env": "REDMINE_RO_DB_PASSWORD", + } + write_inputs(work, APP, read) + db = psycopg2.connect(ro["dsn"]) + db.autocommit = True + + def one(sql: str, args: tuple = ()) -> object: + with db.cursor() as cur: + cur.execute(sql, args) + rows = cur.fetchall() + if len(rows) != 1: + raise RuntimeError(f"{sql} {args}: {len(rows)} rows") + return rows[0][0] + + def project(name: str) -> int: + return int(one("SELECT id FROM projects WHERE name=%s", (name,))) + + def user(name: str) -> int: + return int(one("SELECT id FROM users WHERE firstname || ' ' || lastname = %s", (name,))) + + def enum(kind: str, name: str) -> int: + return int(one("SELECT id FROM enumerations WHERE type=%s AND name=%s", (kind, name))) + + def issue(proj: str, subject: str) -> int: + return int(one("SELECT i.id FROM issues i JOIN projects p ON p.id=i.project_id " + "WHERE p.name=%s AND i.subject=%s", (proj, subject))) # fmt: skip + + def field(name: str) -> int: + return int(one("SELECT id FROM custom_fields WHERE name=%s", (name,))) + + def api(method: str, path: str, payload: object) -> None: + r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r.raise_for_status() + + def create_payload(v: dict) -> dict: + body: dict = { + "project_id": project(v["project"]), + "tracker_id": int(one("SELECT id FROM trackers WHERE name=%s", (v["tracker"],))), + "subject": v["subject"], + "priority_id": enum("IssuePriority", v["priority"]), + } + if v.get("assignee"): + body["assigned_to_id"] = user(v["assignee"]) + if v.get("due_date"): + body["due_date"] = v["due_date"] + if v.get("description"): + body["description"] = v["description"] + cf = [ + {"id": field(n), "value": v[k]} + for k, n in (("customer_ref", "Customer Ref"), ("environment", "Environment")) + if v.get(k) is not None + ] + if cf: + body["custom_fields"] = cf + return {"issue": body} + + for k in setup_view(APP)["kinds"]: + kind = k["kind"] + for ex in k["examples"]: + v = ex["values"] + if kind in ("create_issue", "ensure_issue"): + if kind == "ensure_issue" and int(one( + "SELECT COUNT(*) FROM issues i JOIN projects p ON p.id=i.project_id " + "WHERE p.name=%s AND i.subject=%s", (v["project"], v["subject"]))): # fmt: skip + continue + demo(work, kind, v, lambda p=create_payload(v): api("POST", "/issues.json", p)) + elif kind == "update_issue": + body: dict = {} + if v.get("status"): + body["status_id"] = int(one("SELECT id FROM issue_statuses WHERE name=%s", (v["status"],))) + if v.get("assignee"): + body["assigned_to_id"] = user(v["assignee"]) + if v.get("priority"): + body["priority_id"] = enum("IssuePriority", v["priority"]) + for key in ("due_date", "done_ratio", "notes"): + if v.get(key) is not None: + body[key] = v[key] + iid = issue(v["project"], v["subject"]) + demo(work, kind, v, lambda i=iid, b=body: api("PUT", f"/issues/{i}.json", {"issue": b})) + elif kind == "log_time": + body = {"issue_id": issue(v["project"], v["issue_subject"]), "hours": v["hours"], + "spent_on": v["spent_on"], "activity_id": enum("TimeEntryActivity", v["activity"]), + "comments": v["comments"]} # fmt: skip + demo(work, kind, v, lambda b=body: api("POST", "/time_entries.json", {"time_entry": b})) + bot_id = one("SELECT id FROM users WHERE login=%s", (bot["login"],)) + report = propose(work, {"bot_user_id": str(bot_id)}) + print("questions:", len(report["questions"])) + fx.reset() + + +if __name__ == "__main__": + main(Path(sys.argv[1])) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md new file mode 100644 index 0000000000..53aab255ce --- /dev/null +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -0,0 +1,216 @@ +# Generate verify contracts from a SQL schema + +`python -m openadapt_flow.generate.verify_contract` writes the +`openadapt.verify/v1` contracts for an app's kinds of write. You give it a +read-only database account, a short description of each kind of write, and +one demonstration write per example. It asks a person only what it can't +decide alone, in a numbered list. The goal is three questions or fewer per +app. + +Use it for any app whose data is in MySQL, MariaDB or PostgreSQL. For a FHIR +server, use `openadapt_flow.generate.ensure_contract` (an +`openadapt.ensure-record/v1` contract). This guide needs no code reading. + +## What you need + +- **A read-only database account.** The contracts read with it, and so does + the generator. Put its connection in `read.yaml`, with the password in an + environment variable: + + ```yaml + sql_driver: pymysql # or psycopg2 for PostgreSQL + sql_connect_args: {host: 127.0.0.1, port: 3306, user: oa_reader, database: app} + sql_password_env: APP_READER_PASSWORD + ``` + + The generator refuses an account that can insert, update, delete or change + the schema. On MySQL and MariaDB the account also needs the `TRIGGER` + privilege, because the whole-database check must see triggers. The + generator can grant it (see [Setup steps it applies](#setup-steps-it-applies)). +- **`kinds.yaml`**: each kind of write, its parameter names, and one or two + example tasks. A null value means the task leaves that field out. A list + value (invoice lines) is a list of mappings. + + ```yaml + kinds: + - kind: update_customer + parameters: [customer, town, new_phone, new_email, new_address, new_zip] + examples: + - values: {customer: Transports Petit, town: Rouen, new_phone: 02 35 15 60 60} + - values: {customer: Pharmacie Moreau, town: Lille, new_phone: 03 20 06 07 08, + new_email: officine@pharmacie-moreau-lille.fr} + ``` + +- **The acting user's id**, as the caller passes it to the verifier, for + example `--actor-param bot_user_id=5`. A column that holds it (an author or + creator column) is checked against it. + +## Steps + +1. Check the account and read the schema: + + ```bash + python -m openadapt_flow.generate.verify_contract schema --read read.yaml + ``` + +2. For each example, make the write once, correctly, the way the agent will + (through the app's API or screens), between a snapshot and a record: + + ```bash + python -m openadapt_flow.generate.verify_contract snapshot --read read.yaml --out work/before.json + # ... make the write ... + python -m openadapt_flow.generate.verify_contract record --read read.yaml \ + --before work/before.json --kind update_customer \ + --values '{"customer": "Transports Petit", "town": "Rouen", "new_phone": "02 35 15 60 60"}' \ + --demos work/demos + ``` + + Skip an example whose record already exists ("make sure X exists"): the + right outcome there is no write. Snapshots hold row values. Keep the work + folder private, like a database export. + +3. Propose: + + ```bash + python -m openadapt_flow.generate.verify_contract propose --read read.yaml \ + --kinds kinds.yaml --demos work/demos --out work/out --actor-param bot_user_id=5 + ``` + +4. Read `work/out/QUESTIONS.md`. Write the answers in `answers.yaml` and run + `propose` again with `--answers answers.yaml`. Until every question is + answered, the manifest says so and `choose` refuses to pick a contract. + +## What it writes + +- `contracts//..yaml`: one contract per field set. The + flavor is `create` and `found` for a kind that adds a record, `update` for a + kind that changes one. +- `manifest.yaml`: which contract fits which task values. +- `QUESTIONS.md`: the numbered questions, the proposed answers, and per kind + how many contracts were written and why any were not. +- `report.json`: where each parameter was found, the rows the app adds on its + own, the tables treated as logs, and every contract that was not written, + with the reason. + +At run time, pick the contract for a task with `choose`, then call the +verifier as usual: + +```python +from openadapt_flow.generate.verify_contract import choose +from openadapt_flow.verifier import Actor, begin_write, verify_write + +contract, params = choose("work/out/manifest.yaml", "update_customer", task_values, + {"bot_user_id": "5"}) +pending = begin_write(contract, params, actor=Actor("agent", "bot")) +... # the agent writes +result = verify_write(pending) +``` + +`choose` raises `GenerateError` when no contract fits the task's values. Treat +that as a stop: the write must not be sent unchecked. + +## How it decides + +**The main record.** In each demonstration, the main record is the added row +(a create) or changed row (an update) that holds the most task values. + +**Where a value is.** Each value is looked up by exact rules only: + +| Rule | Example | +| --- | --- | +| The stored text equals the value | `subject` | +| Equal after case folding (an email address) | `Ana@Example.org` | +| Equal digits (a phone number) | `02 38 53 21 09` stored as `0238532109` | +| Equal as a number, a date or a date-time, compared in SQL | `9.20` stored as `9.2`; `2026-10-27 14:30` stored as `2026-10-27 14:30:00` | +| Minutes between two date-time columns of the row | a call's duration | +| A display column of the row a key column points to | `project_id` -> `projects.name` | +| Two text columns joined by one space | `firstname` and `lastname` | +| A row the write added that holds the main record's key | a journal note, a custom field value | + +A key column can be a declared foreign key, or a column whose value is +another table's primary key. When several places fit in every demonstration, +the generator keeps the shortest path, then a declared key, then a column +named after its table, then the most exact rule. If places are still tied, +that is a question. + +An update parameter that no example gives, such as `new_zip`, is placed at the +main table's column with the same name after a leading `new_` or `set_`, when +there is one. The report lists such parameters under `placed_by_name`. + +**Field sets.** A task's field set is its non-null values. For an update, the +generator writes a contract for every non-empty set of the fields to change +that it found, up to 64. For a create, it writes one per demonstrated set, +plus the sets with an optional field (null in some example) left out. A +field left out of a create must be empty in the new record. A list parameter +gets a contract per length, from 1 to 10 items. + +**Create, found, update.** + +- `create`: exactly one new main record holds the task's values, and + everything else in the new row is what the demonstrations showed, or is + bookkeeping. +- `found`: the record already exists, and nothing in the database changed. + `choose` reads the store first and picks `found` when a record with the + task's values is already there. +- `update`: exactly one record matches the task's identity values and holds + the new values. Only the fields the task changes may change. + +**Rows the app adds on its own.** A row type the app added the same number of +times in every demonstration (an agenda event, a stream note, a price-history +row) is declared with that count and its constant columns. A table whose +added rows vary between demonstrations, or whose name says log, history, +audit or similar, is proposed as a log. Columns that changed with no task +value (a modified time, a version counter) are proposed as bookkeeping. +Both go in one question. + +**The frame.** On MySQL and MariaDB every contract uses the whole-database +frame (`frame.database`): a row added, removed or changed in any table that +the contract doesn't declare stops the write. The main record's table and +the tables of rows that hold values are read in full by the contract's own +reads, with every column. On PostgreSQL, `frame.database` isn't available, so +every other table is read as its own surface; tables proposed as logs are +not read there. + +**The recheck window.** On MySQL and MariaDB, one question asks how long the +app can still write on its own after a verified write. The answer is +`frame.database.recheck_after_s`; the proposal is 300 seconds. + +## Setup steps it applies + +The generator never leaves a step for a person to apply later. A contract +that needs something the store doesn't have is either set up, with a flag +and an admin connection you give, or not written, with the reason. + +- `--grant-trigger --admin-read admin.yaml --reader-account "'oa_reader'@'%'"`: + grants `TRIGGER` on the database to the read account (MySQL, MariaDB). + Without it, the generator stops and prints the `GRANT` statement for the + DBA. Note that `TRIGGER` also lets the account create triggers. The verifier + needs it only to see them. +- `--create-views --admin-read admin.yaml --reader-account ...`: the + verifier's SQL guard refuses some table names in a read (EspoCRM's `call`). + This creates a view `oa_read_
` and grants the read account `SELECT` + on it. Without the flag, the kind is refused with the reason. + +Every contract is loaded and read once against the live store before it is +written out. A contract whose read fails is not written; `report.json` says +why. + +## Limits + +- A parameter that no example gives is placed only by the name rule above. + Otherwise a task that gives it has no contract and is stopped. Add an + example and a demonstration. +- A value the demonstrations hold in no row, or in a place none of the rules + describe, is a question. +- A list item that appears twice in one task (the same product and quantity + twice) is stopped: each item must match one new row. +- Columns of a new record that differ between demonstrations and aren't + task values (an invoice's computed totals, its draft reference) are not + checked. The bookkeeping question lists them. +- On PostgreSQL there is no whole-database frame and no recheck: tables + proposed as logs are not read, and a write that lands after the check is + not seen. +- REST metadata (OpenAPI) is not read yet. Apps without database access need + hand-written contracts. +- The generator does not read an audit trail, so another user's edit during + the window stops the write instead of being attributed to them. diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 16f0a543b9..b76d765c5e 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -99,7 +99,10 @@ def rule_for(stored: Optional[str], column_kind: str, value: Any) -> Optional[st text = value_text(value) if text == "": return None - if stored == text: + if stored == text and column_kind not in ("number", "bool", "date", "datetime"): + # A number or date column is compared by value even when this + # demonstration's text happens to match: another row of the same + # column may print as ``5.5000``. return "exact" if column_kind == "text": if "@" in text and stored.casefold() == text.casefold(): diff --git a/packages/flow/openadapt_flow/generate/ensure_contract.py b/packages/flow/openadapt_flow/generate/ensure_contract.py index 4660fd1455..a34a3747bb 100644 --- a/packages/flow/openadapt_flow/generate/ensure_contract.py +++ b/packages/flow/openadapt_flow/generate/ensure_contract.py @@ -824,7 +824,12 @@ def render_yaml(gen: Generated, *, generator: str = "") -> str: def main(argv: Optional[Sequence[str]] = None) -> int: - ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap = argparse.ArgumentParser( + description=__doc__.splitlines()[0] + + " For an app whose data is in MySQL, MariaDB or PostgreSQL, use " + "`python -m openadapt_flow.generate.verify_contract` instead " + "(docs/GENERATE_CONTRACTS.md)." + ) ap.add_argument("teach", help="teach directory") ap.add_argument("--out", help="write the contract YAML here when admitted") ap.add_argument("--report", help="write the JSON report here") diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 94d9aabfad..a69f8d0a82 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1130,7 +1130,7 @@ def _raw_effect( c = t.column(col) lit = value.get("literal") if isinstance(value, dict) else None if c is not None and lit is not None and not ( - c.kind == "text" or (c.kind in ("number", "bool") and re.fullmatch(r"-?\d+", lit)) + c.kind == "text" or (c.kind in ("number", "bool") and c.data_type.startswith(("int", "tinyint", "smallint", "mediumint", "bigint", "integer")) and re.fullmatch(r"-?\d+", lit)) ): m.pop(col) nrf = set(getattr(surf, "_new_row_fields", set())) From debc57a6727c4338808fc566aecb2080962f2ed9 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 13:48:18 -0400 Subject: [PATCH 45/94] WIP: free text unchecked on new records; co-owned child rows; demos for EspoCRM and Redmine --- .../generate_contracts/demo_dolibarr.py | 9 +- .../generate_contracts/demo_espocrm.py | 125 ++++++++++++++++++ .../openadapt_flow/generate/demo_analysis.py | 6 + .../generate/verify_contract.py | 41 +++++- 4 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/demo_espocrm.py diff --git a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py index 4fd6a683ac..45023b6949 100644 --- a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py +++ b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py @@ -63,14 +63,19 @@ def product(ref: str) -> tuple[int, str, str]: kind = k["kind"] if kind == "create_customer": if customer(v["name"], v["town"]) is not None: - continue + # Already there (a "make sure" example): demonstrate a + # second create with values of our own instead. + v = {**v, "name": "Atelier Démo Générateur", "address": "8 place du Marché", + "zip": "21000", "town": "Dijon", "phone": "03 80 30 31 32", + "email": "atelier@demo-generateur.example"} payload = {"name": v["name"], "address": v["address"], "zip": v["zip"], "town": v["town"], "country_id": country(v["country"]), "phone": v["phone"], "email": v["email"], "client": 1} # fmt: skip demo(work, kind, v, lambda p=payload: api("POST", "/thirdparties", p)) elif kind == "create_product": db.commit() if q("SELECT 1 FROM llx_product WHERE BINARY ref=%s", (v["ref"],)): - continue + v = {**v, "ref": "GEN-DEMO-2", "label": "Coffret démo générateur", + "price_excl_vat": "12.40", "vat_rate": "20"} payload = {"ref": v["ref"], "label": v["label"], "type": 0, "price": v["price_excl_vat"], "price_base_type": "HT", "tva_tx": v["vat_rate"], "status": 1, "status_buy": 1} # fmt: skip demo(work, kind, v, lambda p=payload: api("POST", "/products", p)) diff --git a/packages/flow/benchmark/generate_contracts/demo_espocrm.py b/packages/flow/benchmark/generate_contracts/demo_espocrm.py new file mode 100644 index 0000000000..6d13b7ccaa --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/demo_espocrm.py @@ -0,0 +1,125 @@ +"""EspoCRM (first study, instance 2): demonstrations, then the generator. + + FCG_INSTANCE=2 python benchmark/generate_contracts/demo_espocrm.py WORKDIR + +One demonstration write per example in ``setup_view.yaml``, through +EspoCRM's REST API as the API user ``crm-bot``. Record ids for names are +looked up through the read-only account (exact, byte for byte: the name +columns use a case- and accent-insensitive collation). +""" + +from __future__ import annotations + +import datetime as dt +import sys +from pathlib import Path + +import pymysql +import requests + +from demo_lib import demo, fixture, propose, setup_view, write_inputs + +APP = "espocrm" + + +def main(work: Path, extra: list[str]) -> None: + fx = fixture(APP) + fx.reset() + bot = fx.bot() + ro = fx.read_access() + read = { + "sql_driver": "pymysql", + "sql_connect_args": {"host": ro["host"], "port": ro["port"], "user": ro["user"], + "database": ro["database"], "charset": "utf8mb4"}, + "sql_password_env": "ESPOCRM_DB_RO_PASSWORD", + } # fmt: skip + write_inputs(work, APP, read) + db = pymysql.connect(host=ro["host"], port=ro["port"], user=ro["user"], password=ro["password"], + database=ro["database"], charset="utf8mb4", autocommit=True) # fmt: skip + + def one(sql: str, args: tuple = ()) -> str: + with db.cursor() as cur: + cur.execute(sql, args) + rows = cur.fetchall() + if len(rows) != 1: + raise RuntimeError(f"{sql} {args}: {len(rows)} rows") + return str(rows[0][0]) + + def account(name: str, city: str) -> str: + return one("SELECT id FROM account WHERE BINARY name=%s AND BINARY billing_address_city=%s AND deleted=0", (name, city)) + + def person(table: str, full: str, company: str | None = None) -> str: + first, last = full.split(" ", 1) + sql = f"SELECT id FROM {table} WHERE BINARY first_name=%s AND BINARY last_name=%s AND deleted=0" + args: tuple = (first, last) + if company is not None: + sql += " AND BINARY account_name=%s" + args += (company,) + return one(sql, args) + + def api(method: str, path: str, payload: dict) -> None: + r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r.raise_for_status() + + bot_id = one("SELECT id FROM user WHERE user_name=%s", ("crm-bot",)) + for k in setup_view(APP)["kinds"]: + kind = k["kind"] + for ex in k["examples"]: + v = ex["values"] + if kind == "create_contact": + with db.cursor() as cur: + cur.execute("SELECT COUNT(*) FROM contact WHERE BINARY first_name=%s AND BINARY last_name=%s AND deleted=0", + (v["first_name"], v["last_name"])) # fmt: skip + if cur.fetchone()[0]: + continue + body = {"firstName": v["first_name"], "lastName": v["last_name"], + "accountId": account(v["account"], v["account_city"])} # fmt: skip + if v.get("title"): + body["title"] = v["title"] + if v.get("email"): + body["emailAddress"] = v["email"] + if v.get("phone"): + body["phoneNumber"] = v["phone"] + demo(work, kind, v, lambda b=body: api("POST", "/Contact", b)) + elif kind == "create_opportunity": + with db.cursor() as cur: + cur.execute("SELECT COUNT(*) FROM opportunity WHERE BINARY name=%s AND deleted=0", (v["name"],)) + if cur.fetchone()[0]: + continue + body = {"name": v["name"], "accountId": account(v["account"], v["account_city"]), + "amount": v["amount"], "amountCurrency": v["currency"], "stage": v["stage"], + "closeDate": v["close_date"]} # fmt: skip + demo(work, kind, v, lambda b=body: api("POST", "/Opportunity", b)) + elif kind == "update_lead": + lid = one("SELECT id FROM lead WHERE BINARY first_name=%s AND BINARY last_name=%s " + "AND BINARY account_name=%s AND deleted=0", + (v["first_name"], v["last_name"], v["company"])) # fmt: skip + if v["field"] == "email": + body = {"emailAddressData": [{"emailAddress": v["value"], "primary": True, + "optOut": False, "invalid": False}]} # fmt: skip + else: + body = {v["field"]: v["value"]} + demo(work, kind, v, lambda i=lid, b=body: api("PUT", f"/Lead/{i}", b)) + elif kind == "log_call": + start = dt.datetime.strptime(v["date_start"], "%Y-%m-%d %H:%M") + end = start + dt.timedelta(minutes=int(v["duration_minutes"])) + body = {"name": v["subject"], "status": v["status"], "direction": v["direction"], + "dateStart": start.strftime("%Y-%m-%d %H:%M:%S"), + "dateEnd": end.strftime("%Y-%m-%d %H:%M:%S"), "assignedUserId": bot_id} # fmt: skip + if v.get("account"): + body.update(parentType="Account", parentId=account(v["account"], v["account_city"])) + if v.get("lead"): + lead = person("lead", v["lead"], v.get("lead_company")) + body.update(parentType="Lead", parentId=lead, leadsIds=[lead]) + if v.get("contact"): + body["contactsIds"] = [person("contact", v["contact"])] + if v.get("description"): + body["description"] = v["description"] + demo(work, kind, v, lambda b=body: api("POST", "/Call", b)) + report = propose(work, {"bot_user_id": bot_id}, extra) + print("questions:", len(report["questions"])) + fx.reset() + + +if __name__ == "__main__": + main(Path(sys.argv[1]), sys.argv[2:]) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index b76d765c5e..a166aa5141 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -572,6 +572,12 @@ def column_roles( out[name] = ColumnRole("copied", detail=copied) continue distinct = {v for v in values} + if col.kind == "text" and distinct <= {None, ""}: + # Free text the task doesn't give and the demonstrations left + # empty (a line's description, a note): an agent may fill it + # from the record it copies, so it isn't checked. + out[name] = ColumnRole("free") + continue if len(distinct) == 1: v = values[0] default = col.default diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index a69f8d0a82..1943708792 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -185,6 +185,10 @@ class ChildGroup: #: List parameter whose items these rows are, if any. list_param: Optional[str] = None changed_columns: list[str] = field(default_factory=list) + #: Other parameters this row also points to (a contact's account link + #: row also holds the title): with one of them given and this one not, + #: the row is still expected, with this value empty. + co_owners: list[str] = field(default_factory=list) @dataclass @@ -429,12 +433,25 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: break explained = {loc.hops[0].column, *rel.columns} explained |= {h.column for h in rel.hops[:1]} + co: list[str] = [] + for q in plan.params: + if q == p or not rows: + continue + hits = [ + locate(schema, d, _as_change(loc.hops[0].table, r), d.values[q], max_hops=1) + for d, r in rows + if d.values.get(q) is not None and not isinstance(d.values.get(q), (list, dict)) + ] + if hits and all(h and h[0].hops for h in hits): + co.append(q) + explained |= {h[0].hops[0].column for h in hits} plan.children[p] = ChildGroup( loc.hops[0].table, loc.hops[0], {p: rel}, column_roles(schema, loc.hops[0].table, rows, explained, {}), 1, + co_owners=co, ) for p in lists: per_key: dict[str, list[list[Location]]] = {} @@ -906,7 +923,11 @@ def build_contract( # Rows beside the main record that hold a value (a note, a custom field, # an email address link), and list items (invoice lines). if flavor != "found": - groups = [(p, g) for p, g in plan.children.items() if p in fields] + groups = [ + (p, g) + for p, g in plan.children.items() + if p in fields or (plan.mode == "create" and set(g.co_owners) & fields) + ] groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] for p, g in groups: name = f"rows:{g.table}" @@ -940,6 +961,14 @@ def build_contract( m[f"main__{_alias(ip)}"] = _value_expr(ip, loc.rule) for k, loc in g.params.items(): pname = k if g.list_param is None else f"{p}.{i}.{k}" + if g.list_param is None and pname not in fields: + # Given another owner but not this value: empty. + for c in loc.columns if not loc.hops else [loc.hops[0].column]: + alias = f"empty__{_alias(c)}" + if alias not in {a for a, _ in surf.columns}: + surf.columns.append((alias, b.empty("t", g.table, c))) + m[alias] = {"literal": "1"} + continue key = _place(b, surf, g.table, pname, loc) if loc.rule in ("number", "date", "datetime", "minutes"): m[key] = {"literal": "1"} @@ -1141,7 +1170,7 @@ def _raw_effect( if role is not None and role.role == "constant" and role.value is None: continue if role is None or role.role != "constant" or (role.detail or {}).get("is_default"): - if role is None or role.role in ("key", "timestamp", "link", "varies", "copied", "actor"): + if role is None or role.role in ("key", "timestamp", "link", "varies", "copied", "actor", "free"): nrf.add(c.name) continue nrf.add(c.name) @@ -1724,6 +1753,7 @@ def propose( "main_table": p.anchor, "locations": {k: v.describe(p.anchor) for k, v in sorted(p.locations.items())}, "placed_by_name": sorted(p.by_name), + "demonstrations": len(p.demos), "children": {k: g.table for k, g in p.children.items()}, "lists": {k: g.table for k, g in p.lists.items()}, "side_rows": [{"table": s.table, "count": s.count, "unless": s.unless} for s in p.sides], @@ -1805,6 +1835,13 @@ def _questions_md(questions: Sequence[Question], answers: Mapping[str, Any], rep lines.append("") for kind, rep in report["kinds"].items(): lines.append(f"- `{kind}` ({rep['mode'] or 'no contract'}, main table `{rep['main_table']}`): {rep['emitted']} contract(s).") + if rep.get("demonstrations") == 1 and rep["mode"] == "create": + lines.append( + " - One demonstration only: every column of a new record that the task " + "doesn't give must hold the value it held then. Record a second create " + "with other values, so a column computed from the values (a price with " + "tax) is told apart from a constant." + ) for r in rep["refused"]: lines.append(f" - Refused: {r}") for r in rep["rejected"][:5]: From f3361fb2d97170cef09b767b1e02c9cd70e746b3 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:03:22 -0400 Subject: [PATCH 46/94] WIP: format; summarize script; connection hygiene in demos --- .../generate_contracts/demo_dolibarr.py | 79 ++- .../generate_contracts/demo_espocrm.py | 25 +- .../benchmark/generate_contracts/demo_lib.py | 12 +- .../generate_contracts/demo_redmine.py | 62 ++- .../benchmark/generate_contracts/summarize.py | 81 +++ .../benchmark/generate_contracts/validate.py | 39 +- .../openadapt_flow/generate/demo_analysis.py | 58 +- .../flow/openadapt_flow/generate/sql_store.py | 26 +- .../generate/verify_contract.py | 523 ++++++++++++++---- .../tests/test_generate_verify_contract.py | 141 ++++- 10 files changed, 840 insertions(+), 206 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/summarize.py diff --git a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py index 45023b6949..85bcc4a74a 100644 --- a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py +++ b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py @@ -15,7 +15,6 @@ import pymysql import requests - from demo_lib import demo, fixture, propose, setup_view, write_inputs APP = "dolibarr" @@ -28,11 +27,23 @@ def main(work: Path) -> None: ro = fx.read_access() read = { "sql_driver": "pymysql", - "sql_connect_args": {"host": ro["host"], "port": ro["port"], "user": ro["user"], "database": ro["database"]}, + "sql_connect_args": { + "host": ro["host"], + "port": ro["port"], + "user": ro["user"], + "database": ro["database"], + }, "sql_password_env": "FCG_DOLIBARR_READER_PASSWORD", } write_inputs(work, APP, read) - db = pymysql.connect(host=ro["host"], port=ro["port"], user=ro["user"], password=ro["password"], database=ro["database"]) + db = pymysql.connect( + host=ro["host"], + port=ro["port"], + user=ro["user"], + password=ro["password"], + database=ro["database"], + autocommit=True, + ) def q(sql: str, args: tuple = ()) -> list[tuple]: with db.cursor() as cur: @@ -40,13 +51,22 @@ def q(sql: str, args: tuple = ()) -> list[tuple]: return list(cur.fetchall()) def api(method: str, path: str, payload: object = None) -> object: - r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r = requests.request( + method, + bot["base_url"] + path, + headers=bot["headers"], + json=payload, + timeout=60, + ) r.raise_for_status() return r.json() def customer(name: str, town: str) -> int | None: db.commit() - rows = q("SELECT rowid FROM llx_societe WHERE BINARY nom=%s AND BINARY town=%s", (name, town)) + rows = q( + "SELECT rowid FROM llx_societe WHERE BINARY nom=%s AND BINARY town=%s", + (name, town), + ) return int(rows[0][0]) if len(rows) == 1 else None def country(label: str) -> int: @@ -54,7 +74,9 @@ def country(label: str) -> int: def product(ref: str) -> tuple[int, str, str]: db.commit() - rid, price, tva = q("SELECT rowid, price, tva_tx FROM llx_product WHERE BINARY ref=%s", (ref,))[0] + rid, price, tva = q( + "SELECT rowid, price, tva_tx FROM llx_product WHERE BINARY ref=%s", (ref,) + )[0] return int(rid), str(price), str(tva) for k in setup_view(APP)["kinds"]: @@ -65,17 +87,28 @@ def product(ref: str) -> tuple[int, str, str]: if customer(v["name"], v["town"]) is not None: # Already there (a "make sure" example): demonstrate a # second create with values of our own instead. - v = {**v, "name": "Atelier Démo Générateur", "address": "8 place du Marché", - "zip": "21000", "town": "Dijon", "phone": "03 80 30 31 32", - "email": "atelier@demo-generateur.example"} + v = { + **v, + "name": "Atelier Démo Générateur", + "address": "8 place du Marché", + "zip": "21000", + "town": "Dijon", + "phone": "03 80 30 31 32", + "email": "atelier@demo-generateur.example", + } payload = {"name": v["name"], "address": v["address"], "zip": v["zip"], "town": v["town"], "country_id": country(v["country"]), "phone": v["phone"], "email": v["email"], "client": 1} # fmt: skip demo(work, kind, v, lambda p=payload: api("POST", "/thirdparties", p)) elif kind == "create_product": db.commit() if q("SELECT 1 FROM llx_product WHERE BINARY ref=%s", (v["ref"],)): - v = {**v, "ref": "GEN-DEMO-2", "label": "Coffret démo générateur", - "price_excl_vat": "12.40", "vat_rate": "20"} + v = { + **v, + "ref": "GEN-DEMO-2", + "label": "Coffret démo générateur", + "price_excl_vat": "12.40", + "vat_rate": "20", + } payload = {"ref": v["ref"], "label": v["label"], "type": 0, "price": v["price_excl_vat"], "price_base_type": "HT", "tva_tx": v["vat_rate"], "status": 1, "status_buy": 1} # fmt: skip demo(work, kind, v, lambda p=payload: api("POST", "/products", p)) @@ -84,17 +117,35 @@ def product(ref: str) -> tuple[int, str, str]: lines = [] for ln in v["lines"]: pid, price, tva = product(ln["product_ref"]) - lines.append({"fk_product": pid, "qty": ln["qty"], "subprice": price, "tva_tx": tva, "product_type": 0}) + lines.append( + { + "fk_product": pid, + "qty": ln["qty"], + "subprice": price, + "tva_tx": tva, + "product_type": 0, + } + ) payload = {"socid": customer(v["customer"], v["town"]), "type": 0, "date": calendar.timegm((y, m, d, 12, 0, 0, 0, 0, 0)), "lines": lines} # fmt: skip demo(work, kind, v, lambda p=payload: api("POST", "/invoices", p)) elif kind == "update_customer": cid = customer(v["customer"], v["town"]) - change = {key[4:]: x for key, x in v.items() if key.startswith("new_") and x is not None} - demo(work, kind, v, lambda c=cid, p=change: api("PUT", f"/thirdparties/{c}", p)) + change = { + key[4:]: x + for key, x in v.items() + if key.startswith("new_") and x is not None + } + demo( + work, + kind, + v, + lambda c=cid, p=change: api("PUT", f"/thirdparties/{c}", p), + ) bot_id = q("SELECT rowid FROM llx_user WHERE login='fcg_bot'")[0][0] report = propose(work, {"bot_user_id": str(bot_id)}) print("questions:", len(report["questions"])) + db.close() # an open read transaction would block the reset fx.reset() diff --git a/packages/flow/benchmark/generate_contracts/demo_espocrm.py b/packages/flow/benchmark/generate_contracts/demo_espocrm.py index 6d13b7ccaa..3b6c3ca9da 100644 --- a/packages/flow/benchmark/generate_contracts/demo_espocrm.py +++ b/packages/flow/benchmark/generate_contracts/demo_espocrm.py @@ -16,7 +16,6 @@ import pymysql import requests - from demo_lib import demo, fixture, propose, setup_view, write_inputs APP = "espocrm" @@ -46,7 +45,10 @@ def one(sql: str, args: tuple = ()) -> str: return str(rows[0][0]) def account(name: str, city: str) -> str: - return one("SELECT id FROM account WHERE BINARY name=%s AND BINARY billing_address_city=%s AND deleted=0", (name, city)) + return one( + "SELECT id FROM account WHERE BINARY name=%s AND BINARY billing_address_city=%s AND deleted=0", + (name, city), + ) def person(table: str, full: str, company: str | None = None) -> str: first, last = full.split(" ", 1) @@ -58,7 +60,13 @@ def person(table: str, full: str, company: str | None = None) -> str: return one(sql, args) def api(method: str, path: str, payload: dict) -> None: - r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r = requests.request( + method, + bot["base_url"] + path, + headers=bot["headers"], + json=payload, + timeout=60, + ) r.raise_for_status() bot_id = one("SELECT id FROM user WHERE user_name=%s", ("crm-bot",)) @@ -83,7 +91,10 @@ def api(method: str, path: str, payload: dict) -> None: demo(work, kind, v, lambda b=body: api("POST", "/Contact", b)) elif kind == "create_opportunity": with db.cursor() as cur: - cur.execute("SELECT COUNT(*) FROM opportunity WHERE BINARY name=%s AND deleted=0", (v["name"],)) + cur.execute( + "SELECT COUNT(*) FROM opportunity WHERE BINARY name=%s AND deleted=0", + (v["name"],), + ) if cur.fetchone()[0]: continue body = {"name": v["name"], "accountId": account(v["account"], v["account_city"]), @@ -107,7 +118,10 @@ def api(method: str, path: str, payload: dict) -> None: "dateStart": start.strftime("%Y-%m-%d %H:%M:%S"), "dateEnd": end.strftime("%Y-%m-%d %H:%M:%S"), "assignedUserId": bot_id} # fmt: skip if v.get("account"): - body.update(parentType="Account", parentId=account(v["account"], v["account_city"])) + body.update( + parentType="Account", + parentId=account(v["account"], v["account_city"]), + ) if v.get("lead"): lead = person("lead", v["lead"], v.get("lead_company")) body.update(parentType="Lead", parentId=lead, leadsIds=[lead]) @@ -118,6 +132,7 @@ def api(method: str, path: str, payload: dict) -> None: demo(work, kind, v, lambda b=body: api("POST", "/Call", b)) report = propose(work, {"bot_user_id": bot_id}, extra) print("questions:", len(report["questions"])) + db.close() fx.reset() diff --git a/packages/flow/benchmark/generate_contracts/demo_lib.py b/packages/flow/benchmark/generate_contracts/demo_lib.py index f0f82bf224..40499cc211 100644 --- a/packages/flow/benchmark/generate_contracts/demo_lib.py +++ b/packages/flow/benchmark/generate_contracts/demo_lib.py @@ -48,7 +48,9 @@ def fixture(app: str) -> Any: def setup_view(app: str) -> dict[str, Any]: - return yaml.safe_load((FCG / "apps" / app / "setup_view.yaml").read_text(encoding="utf-8")) + return yaml.safe_load( + (FCG / "apps" / app / "setup_view.yaml").read_text(encoding="utf-8") + ) def write_inputs(work: Path, app: str, read: dict[str, Any]) -> list[str]: @@ -68,11 +70,15 @@ def write_inputs(work: Path, app: str, read: dict[str, Any]) -> list[str]: for k in view["kinds"] ] } - (work / "kinds.yaml").write_text(yaml.safe_dump(kinds, allow_unicode=True), encoding="utf-8") + (work / "kinds.yaml").write_text( + yaml.safe_dump(kinds, allow_unicode=True), encoding="utf-8" + ) return ["--read", str(work / "read.yaml")] -def demo(work: Path, kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: +def demo( + work: Path, kind: str, values: dict[str, Any], write: Callable[[], Any] +) -> None: """Snapshot, make one demonstration write, record the diff.""" flags = ["--read", str(work / "read.yaml")] diff --git a/packages/flow/benchmark/generate_contracts/demo_redmine.py b/packages/flow/benchmark/generate_contracts/demo_redmine.py index 1135204fec..6f643913ad 100644 --- a/packages/flow/benchmark/generate_contracts/demo_redmine.py +++ b/packages/flow/benchmark/generate_contracts/demo_redmine.py @@ -14,7 +14,6 @@ import psycopg2 import requests - from demo_lib import demo, fixture, propose, setup_view, write_inputs APP = "redmine" @@ -27,7 +26,12 @@ def main(work: Path) -> None: ro = fx.read_access() read = { "sql_driver": "psycopg2", - "sql_connect_args": {"host": ro["host"], "port": ro["port"], "user": ro["user"], "dbname": ro["database"]}, + "sql_connect_args": { + "host": ro["host"], + "port": ro["port"], + "user": ro["user"], + "dbname": ro["database"], + }, "sql_password_env": "REDMINE_RO_DB_PASSWORD", } write_inputs(work, APP, read) @@ -46,10 +50,14 @@ def project(name: str) -> int: return int(one("SELECT id FROM projects WHERE name=%s", (name,))) def user(name: str) -> int: - return int(one("SELECT id FROM users WHERE firstname || ' ' || lastname = %s", (name,))) + return int( + one("SELECT id FROM users WHERE firstname || ' ' || lastname = %s", (name,)) + ) def enum(kind: str, name: str) -> int: - return int(one("SELECT id FROM enumerations WHERE type=%s AND name=%s", (kind, name))) + return int( + one("SELECT id FROM enumerations WHERE type=%s AND name=%s", (kind, name)) + ) def issue(proj: str, subject: str) -> int: return int(one("SELECT i.id FROM issues i JOIN projects p ON p.id=i.project_id " @@ -59,13 +67,21 @@ def field(name: str) -> int: return int(one("SELECT id FROM custom_fields WHERE name=%s", (name,))) def api(method: str, path: str, payload: object) -> None: - r = requests.request(method, bot["base_url"] + path, headers=bot["headers"], json=payload, timeout=60) + r = requests.request( + method, + bot["base_url"] + path, + headers=bot["headers"], + json=payload, + timeout=60, + ) r.raise_for_status() def create_payload(v: dict) -> dict: body: dict = { "project_id": project(v["project"]), - "tracker_id": int(one("SELECT id FROM trackers WHERE name=%s", (v["tracker"],))), + "tracker_id": int( + one("SELECT id FROM trackers WHERE name=%s", (v["tracker"],)) + ), "subject": v["subject"], "priority_id": enum("IssuePriority", v["priority"]), } @@ -77,7 +93,10 @@ def create_payload(v: dict) -> dict: body["description"] = v["description"] cf = [ {"id": field(n), "value": v[k]} - for k, n in (("customer_ref", "Customer Ref"), ("environment", "Environment")) + for k, n in ( + ("customer_ref", "Customer Ref"), + ("environment", "Environment"), + ) if v.get(k) is not None ] if cf: @@ -93,11 +112,21 @@ def create_payload(v: dict) -> dict: "SELECT COUNT(*) FROM issues i JOIN projects p ON p.id=i.project_id " "WHERE p.name=%s AND i.subject=%s", (v["project"], v["subject"]))): # fmt: skip continue - demo(work, kind, v, lambda p=create_payload(v): api("POST", "/issues.json", p)) + demo( + work, + kind, + v, + lambda p=create_payload(v): api("POST", "/issues.json", p), + ) elif kind == "update_issue": body: dict = {} if v.get("status"): - body["status_id"] = int(one("SELECT id FROM issue_statuses WHERE name=%s", (v["status"],))) + body["status_id"] = int( + one( + "SELECT id FROM issue_statuses WHERE name=%s", + (v["status"],), + ) + ) if v.get("assignee"): body["assigned_to_id"] = user(v["assignee"]) if v.get("priority"): @@ -106,15 +135,26 @@ def create_payload(v: dict) -> dict: if v.get(key) is not None: body[key] = v[key] iid = issue(v["project"], v["subject"]) - demo(work, kind, v, lambda i=iid, b=body: api("PUT", f"/issues/{i}.json", {"issue": b})) + demo( + work, + kind, + v, + lambda i=iid, b=body: api("PUT", f"/issues/{i}.json", {"issue": b}), + ) elif kind == "log_time": body = {"issue_id": issue(v["project"], v["issue_subject"]), "hours": v["hours"], "spent_on": v["spent_on"], "activity_id": enum("TimeEntryActivity", v["activity"]), "comments": v["comments"]} # fmt: skip - demo(work, kind, v, lambda b=body: api("POST", "/time_entries.json", {"time_entry": b})) + demo( + work, + kind, + v, + lambda b=body: api("POST", "/time_entries.json", {"time_entry": b}), + ) bot_id = one("SELECT id FROM users WHERE login=%s", (bot["login"],)) report = propose(work, {"bot_user_id": str(bot_id)}) print("questions:", len(report["questions"])) + db.close() fx.reset() diff --git a/packages/flow/benchmark/generate_contracts/summarize.py b/packages/flow/benchmark/generate_contracts/summarize.py new file mode 100644 index 0000000000..efe88d438c --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/summarize.py @@ -0,0 +1,81 @@ +"""Count a validation run: correct writes confirmed, faults stopped, by kind. + + python benchmark/generate_contracts/summarize.py APP WORKDIR + +A case passes when the verifier says CONFIRMED. The oracle's label says what +the write really did: ``correct`` (or no write needed), or harm. A correct +write that is not CONFIRMED is a false stop; a harm that is CONFIRMED is a +miss. Fault scope (in or out of what the verifier claims) comes from the +pack's ``faults.yaml``. +""" + +from __future__ import annotations + +import json +import sys +from collections import defaultdict +from pathlib import Path + +import yaml +from demo_lib import FCG + +GOOD = { + "correct" +} # "nothing" (no write where one was needed) and "harm" must be stopped + + +def main() -> None: + app, work = sys.argv[1], Path(sys.argv[2]) + scope = { + f["id"]: f.get("scope", "in") + for f in yaml.safe_load( + (FCG / "apps" / app / "faults" / "faults.yaml").read_text() + ) + } + rows = [ + json.loads(x) + for x in (work / "validate.jsonl").read_text().splitlines() + if x.strip() + ] + by_kind: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int)) + misses, false_stops = [], [] + for r in rows: + k = r["kind"] + ok = r["verdict"]["outcome"] == "CONFIRMED" + label = str(r["oracle"].get("label")) + harm = label not in GOOD + sc = scope.get(r["case"].split("-", 1)[1], "in") + if harm: + by_kind[k][f"harm_{sc}"] += 1 + if ok: + by_kind[k][f"harm_{sc}_confirmed"] += 1 + misses.append((r["case"], sc, label)) + else: + by_kind[k]["correct"] += 1 + if ok: + by_kind[k]["correct_confirmed"] += 1 + else: + false_stops.append( + ( + r["case"], + r["verdict"]["outcome"], + "; ".join(r["verdict"]["reasons"])[:160], + ) + ) + print( + "| Kind | Correct writes confirmed | In-scope harms stopped | Out-of-scope harms stopped |" + ) + print("| --- | --- | --- | --- |") + for k, c in sorted(by_kind.items()): + hi, ho = c["harm_in"], c["harm_out"] + print(f"| `{k}` | {c['correct_confirmed']}/{c['correct']} | {hi - c['harm_in_confirmed']}/{hi} | " + f"{ho - c['harm_out_confirmed']}/{ho} |") # fmt: skip + print() + for m in misses: + print("MISS", *m) + for f in false_stops: + print("FALSE STOP", *f) + + +if __name__ == "__main__": + main() diff --git a/packages/flow/benchmark/generate_contracts/validate.py b/packages/flow/benchmark/generate_contracts/validate.py index 65f7eedab3..d7371955bb 100644 --- a/packages/flow/benchmark/generate_contracts/validate.py +++ b/packages/flow/benchmark/generate_contracts/validate.py @@ -23,7 +23,6 @@ from typing import Any import yaml - from demo_lib import FCG, fixture, load from openadapt_flow.generate import verify_contract as gen @@ -37,7 +36,9 @@ def main() -> int: ap.add_argument("app") ap.add_argument("work", type=Path) ap.add_argument("--only", nargs="*") - ap.add_argument("--actor-id", required=True, help="the bot's id as the contracts bind it") + ap.add_argument( + "--actor-id", required=True, help="the bot's id as the contracts bind it" + ) ap.add_argument("--actor-param", default="bot_user_id") ap.add_argument("--no-faults", action="store_true") ap.add_argument("--no-correct", action="store_true") @@ -52,11 +53,20 @@ def main() -> int: out = args.work / "validate.jsonl" done = set() if out.exists(): - done = {json.loads(x)["case"] for x in out.read_text().splitlines() if x.strip()} + done = { + json.loads(x)["case"] for x in out.read_text().splitlines() if x.strip() + } cases: list[tuple[str, dict, Any, float]] = [] if not args.no_correct: for tid, task in tasks.items(): - cases.append((f"correct-{tid}", task, lambda t=task: faults.correct_write(t, fx.bot()), 0.0)) + cases.append( + ( + f"correct-{tid}", + task, + lambda t=task: faults.correct_write(t, fx.bot()), + 0.0, + ) + ) if not args.no_faults: for f in yaml.safe_load((app_dir / "faults" / "faults.yaml").read_text()): task = tasks[f["task"]] @@ -70,7 +80,9 @@ def main() -> int: ) # fmt: skip for case, task, act, late in cases: short = case.split("-", 1)[1] - if case in done or (args.only and short not in args.only and task["id"] not in args.only): + if case in done or ( + args.only and short not in args.only and task["id"] not in args.only + ): continue while os.getloadavg()[0] >= 35: time.sleep(30) @@ -79,17 +91,28 @@ def main() -> int: before = oracle.snapshot() pending = None try: - contract, params = gen.choose(manifest, task["kind"], task["values"], {args.actor_param: args.actor_id}) + contract, params = gen.choose( + manifest, + task["kind"], + task["values"], + {args.actor_param: args.actor_id}, + ) rec["contract"] = contract.name tmp = tempfile.mkdtemp() pending = begin_write(contract, params, actor=Actor("agent", "generated-check"), ledger=None, write_key=f"{case}-{time.time_ns()}", workdir=tmp) # fmt: skip except Exception as exc: # noqa: BLE001 - a stop before the write - rec["verdict"] = {"outcome": "BLOCKED_AT_BEGIN", "reasons": [f"{type(exc).__name__}: {exc}"]} + rec["verdict"] = { + "outcome": "BLOCKED_AT_BEGIN", + "reasons": [f"{type(exc).__name__}: {exc}"], + } try: rec["claim"] = act() except Exception as exc: # noqa: BLE001 - rec["claim"] = {"status": "not_done", "note": f"{type(exc).__name__}: {exc}"} + rec["claim"] = { + "status": "not_done", + "note": f"{type(exc).__name__}: {exc}", + } if pending is not None: try: r = verify_write(pending) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index a166aa5141..02d8bdba7d 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -347,7 +347,12 @@ def locate( added = set() for ch in demo.diff.changes: t2 = schema.tables.get(ch.table) - if ch.kind == "added" and ch.after and t2 is not None and len(t2.primary_key) == 1: + if ( + ch.kind == "added" + and ch.after + and t2 is not None + and len(t2.primary_key) == 1 + ): added.add((ch.table, ch.after.get(t2.primary_key[0]))) def walk( @@ -393,7 +398,9 @@ def walk( for col in child.columns: if ch.after.get(col.name) != key: continue - if links is not None and col.name not in links.get(ch.table, {col.name}): + if links is not None and col.name not in links.get( + ch.table, {col.name} + ): continue declared = any( fk.column == col.name and fk.table == t.name @@ -405,7 +412,11 @@ def walk( child, ch.after, hops - + (Hop("child", ch.table, col.name, t.primary_key[0], declared),), + + ( + Hop( + "child", ch.table, col.name, t.primary_key[0], declared + ), + ), depth + 1, ) @@ -416,7 +427,9 @@ def walk( return sorted(unique.values(), key=_rank) -def link_columns(schema: Schema, demos: Sequence[tuple["Demo", RowChange]]) -> dict[str, set[str]]: +def link_columns( + schema: Schema, demos: Sequence[tuple["Demo", RowChange]] +) -> dict[str, set[str]]: """Per table, the columns that hold the main record's key in every row that table gained, across all demonstrations (a journal's ``journalized_id``, not its ``user_id`` that happens to be equal once).""" @@ -537,10 +550,14 @@ def column_roles( if col.auto or (name in t.primary_key and len(t.primary_key) == 1): out[name] = ColumnRole("key") continue - if col.kind in ("date", "datetime") and all( - v is None or _within(v, d.started_at, d.finished_at) - for (d, _), v in zip(rows, values) - ) and any(v is not None for v in values): + if ( + col.kind in ("date", "datetime") + and all( + v is None or _within(v, d.started_at, d.finished_at) + for (d, _), v in zip(rows, values) + ) + and any(v is not None for v in values) + ): out[name] = ColumnRole("timestamp") continue actor = [ @@ -553,17 +570,22 @@ def column_roles( continue links = link_values or [set() for _ in rows] if values and all( - v is not None and (v in lv or (len(t.primary_key) == 1 and v == r.get(t.primary_key[0]))) + v is not None + and (v in lv or (len(t.primary_key) == 1 and v == r.get(t.primary_key[0]))) for v, lv, (_, r) in zip(values, links, rows) ): out[name] = ColumnRole("link") continue - if len(t.primary_key) == 1 and values and all( - v is not None - and r.get(t.primary_key[0]) is not None - and str(r.get(t.primary_key[0])) in v - and v != r.get(t.primary_key[0]) - for v, (_, r) in zip(values, rows) + if ( + len(t.primary_key) == 1 + and values + and all( + v is not None + and r.get(t.primary_key[0]) is not None + and str(r.get(t.primary_key[0])) in v + and v != r.get(t.primary_key[0]) + for v, (_, r) in zip(values, rows) + ) ): out[name] = ColumnRole("varies", detail={"why": "holds the row's own key"}) continue @@ -585,7 +607,11 @@ def column_roles( out[name] = ColumnRole("constant", None) else: out[name] = ColumnRole( - "constant", v, detail={"is_default": default is not None and default.strip("'") == v} + "constant", + v, + detail={ + "is_default": default is not None and default.strip("'") == v + }, ) continue out[name] = ColumnRole("varies") diff --git a/packages/flow/openadapt_flow/generate/sql_store.py b/packages/flow/openadapt_flow/generate/sql_store.py index df0a7d666c..7feeede142 100644 --- a/packages/flow/openadapt_flow/generate/sql_store.py +++ b/packages/flow/openadapt_flow/generate/sql_store.py @@ -181,7 +181,9 @@ def connector(read: Mapping[str, Any]) -> Callable[[], Any]: env = read.get("sql_password_env") if env: if env not in os.environ: - raise StoreError(f"environment variable {env} (the read password) is not set") + raise StoreError( + f"environment variable {env} (the read password) is not set" + ) args["password"] = os.environ[str(env)] def connect() -> Any: @@ -253,7 +255,9 @@ def _mysql_schema(cur: Any) -> Schema: t.columns.append( Column( name=str(cname), - data_type="tinyint(1)" if ctype.startswith("tinyint(1)") else str(dtype).lower(), + data_type="tinyint(1)" + if ctype.startswith("tinyint(1)") + else str(dtype).lower(), nullable=str(nullable).upper() == "YES", default=None if default is None else str(default), auto="auto_increment" in extra, @@ -438,9 +442,7 @@ def save_snapshot(path: Path, rows: Mapping[str, TableRows]) -> None: def load_snapshot(path: Path) -> dict[str, TableRows]: data = json.loads(path.read_text(encoding="utf-8")) - return { - n: TableRows(dict(t["rows"]), t.get("count_only")) for n, t in data.items() - } + return {n: TableRows(dict(t["rows"]), t.get("count_only")) for n, t in data.items()} @dataclass @@ -476,7 +478,9 @@ def tables(self) -> list[str]: def of(self, table: str, kind: Optional[str] = None) -> list[RowChange]: return [ - c for c in self.changes if c.table == table and (kind is None or c.kind == kind) + c + for c in self.changes + if c.table == table and (kind is None or c.kind == kind) ] def to_json(self) -> dict[str, Any]: @@ -489,7 +493,10 @@ def to_json(self) -> dict[str, Any]: def from_json(cls, data: Mapping[str, Any]) -> "Diff": return cls( [RowChange(**c) for c in data.get("changes", [])], - {k: (int(v[0]), int(v[1])) for k, v in dict(data.get("unread", {})).items()}, + { + k: (int(v[0]), int(v[1])) + for k, v in dict(data.get("unread", {})).items() + }, ) @@ -501,7 +508,10 @@ def diff(before: Mapping[str, TableRows], after: Mapping[str, TableRows]) -> Dif for name in sorted(set(before) | set(after)): a, b = before.get(name), after.get(name) if a is None or b is None: - unread[name] = (0 if a is None else len(a.rows), 0 if b is None else len(b.rows)) + unread[name] = ( + 0 if a is None else len(a.rows), + 0 if b is None else len(b.rows), + ) continue if a.count_only is not None or b.count_only is not None: ca = a.count_only if a.count_only is not None else len(a.rows) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 1943708792..0342476c1e 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -47,11 +47,11 @@ from openadapt_flow.generate import sql_store as store from openadapt_flow.generate.demo_analysis import ( - _rank, ColumnRole, Demo, Hop, Location, + _rank, best, column_roles, is_identifier, @@ -59,9 +59,8 @@ locate, looks_like_log, pick_anchor, - snake, ) -from openadapt_flow.generate.sql_store import RowChange, Schema, Table +from openadapt_flow.generate.sql_store import RowChange, Schema MANIFEST_SCHEMA = "openadapt.contract-manifest/v1" #: Most contracts one kind gets (all subsets of up to six optional fields). @@ -128,7 +127,10 @@ def load_kinds(path: Path) -> list[KindSpec]: [str(p) for p in k["parameters"]], str(k.get("description") or ""), [ - {"instruction": e.get("instruction", ""), "values": dict(e.get("values") or {})} + { + "instruction": e.get("instruction", ""), + "values": dict(e.get("values") or {}), + } for e in k.get("examples") or [] ], ) @@ -234,7 +236,9 @@ class KindPlan: def _scalars(values: Mapping[str, Any]) -> dict[str, Any]: return { - k: v for k, v in values.items() if v is not None and not isinstance(v, (list, dict)) + k: v + for k, v in values.items() + if v is not None and not isinstance(v, (list, dict)) } @@ -286,7 +290,11 @@ def analyze( plan.anchor, plan.mode = anchor.table, mode plan.demos.append(d) plan.anchors.append(anchor) - lists = [p for p in spec.parameters if any(isinstance(d.values.get(p), list) for d in plan.demos)] + lists = [ + p + for p in spec.parameters + if any(isinstance(d.values.get(p), list) for d in plan.demos) + ] links = link_columns(schema, list(zip(plan.demos, plan.anchors))) scalars = [p for p in spec.parameters if p not in lists] @@ -405,7 +413,12 @@ def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: col = schema.tables[plan.anchor].column(name) if col is None or name in schema.tables[plan.anchor].primary_key: return None - rule = {"number": "number", "bool": "number", "date": "date", "datetime": "datetime"}.get(col.kind, "exact") + rule = { + "number": "number", + "bool": "number", + "date": "date", + "datetime": "datetime", + }.get(col.kind, "exact") if col.kind not in ("text", "number", "bool", "date", "datetime"): return None return Location((), (name,), rule) @@ -428,7 +441,13 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: key = (a.after or {}).get(loc.hops[0].ref_column) if ch.after.get(loc.hops[0].column) != key: continue - if locate(schema, d, _as_change(ch.table, ch.after), d.values[p], max_hops=len(rel.hops)): + if locate( + schema, + d, + _as_change(ch.table, ch.after), + d.values[p], + max_hops=len(rel.hops), + ): rows.append((d, ch.after)) break explained = {loc.hops[0].column, *rel.columns} @@ -438,9 +457,12 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: if q == p or not rows: continue hits = [ - locate(schema, d, _as_change(loc.hops[0].table, r), d.values[q], max_hops=1) + locate( + schema, d, _as_change(loc.hops[0].table, r), d.values[q], max_hops=1 + ) for d, r in rows - if d.values.get(q) is not None and not isinstance(d.values.get(q), (list, dict)) + if d.values.get(q) is not None + and not isinstance(d.values.get(q), (list, dict)) ] if hits and all(h and h[0].hops for h in hits): co.append(q) @@ -461,7 +483,11 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: items = d.values.get(p) if not isinstance(items, list): continue - key = (a.after or {}).get(schema.tables[plan.anchor].primary_key[0]) if schema.tables[plan.anchor].primary_key else None + key = ( + (a.after or {}).get(schema.tables[plan.anchor].primary_key[0]) + if schema.tables[plan.anchor].primary_key + else None + ) children = [ ch for ch in d.diff.changes @@ -480,14 +506,14 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: if i in used: continue locs = { - k: locate(schema, d, _as_change(ch.table, ch.after), v, max_hops=1) + k: locate( + schema, d, _as_change(ch.table, ch.after), v, max_hops=1 + ) for k, v in item.items() } if all(locs.values()): used.add(i) - link_col = next( - c for c, v in ch.after.items() if v == key - ) + link_col = next(c for c, v in ch.after.items() if v == key) table = ch.table link = Hop( "child", @@ -514,7 +540,11 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: explained = {link.column} | {c for loc in locs.values() for c in loc.columns} explained |= {loc.hops[0].column for loc in locs.values() if loc.hops} plan.lists[p] = ChildGroup( - table, link, locs, column_roles(schema, table, rows, explained, {}), list_param=p + table, + link, + locs, + column_roles(schema, table, rows, explained, {}), + list_param=p, ) @@ -544,17 +574,27 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N plan.removed[ch.table] = plan.removed.get(ch.table, 0) + 1 continue if ch.kind == "changed": - linked = ch.table in child_tables and a.after is not None and any( - v == (a.after or {}).get(plan_pk(schema, plan.anchor)) - for v in (ch.after or {}).values() + linked = ( + ch.table in child_tables + and a.after is not None + and any( + v == (a.after or {}).get(plan_pk(schema, plan.anchor)) + for v in (ch.after or {}).values() + ) ) if linked: g = plan.children[child_tables[ch.table]] - g.changed_columns = sorted(set(g.changed_columns) | set(ch.changed_columns)) + g.changed_columns = sorted( + set(g.changed_columns) | set(ch.changed_columns) + ) elif ch.table == plan.anchor: - plan.bookkeeping.setdefault(ch.table, set()).update(ch.changed_columns) + plan.bookkeeping.setdefault(ch.table, set()).update( + ch.changed_columns + ) else: - plan.bookkeeping.setdefault(ch.table, set()).update(ch.changed_columns) + plan.bookkeeping.setdefault(ch.table, set()).update( + ch.changed_columns + ) continue # added if ch.table in list_tables: @@ -574,24 +614,23 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N d.__dict__["_added_keys"] = added_keys for t, cs in sorted(counts.items()): rows = [r for group in pure.get(t, []) for r in group] - if ( - len(set(cs)) == 1 - and cs[0] > 0 - and not looks_like_log(t) - and rows - ): + if len(set(cs)) == 1 and cs[0] > 0 and not looks_like_log(t) and rows: links = [getattr(d, "_added_keys", set()) for d, _ in rows] roles = column_roles(schema, t, rows, set(), actor_values, links) - plan.sides.append( - SideSpec(t, cs[0], roles, unless=child_tables.get(t)) - ) + plan.sides.append(SideSpec(t, cs[0], roles, unless=child_tables.get(t))) elif rows: plan.logs.add(t) if plan.mode == "update": for a in plan.anchors: plan.anchor_changed |= set(a.changed_columns) - explained = {c for loc in plan.locations.values() if not loc.hops for c in loc.columns} - explained |= {loc.hops[0].column for loc in plan.locations.values() if loc.hops and loc.hops[0].kind == "fk"} + explained = { + c for loc in plan.locations.values() if not loc.hops for c in loc.columns + } + explained |= { + loc.hops[0].column + for loc in plan.locations.values() + if loc.hops and loc.hops[0].kind == "fk" + } for locs in plan.dispatch_locations.values(): for loc in locs.values(): explained |= set(loc.columns) if not loc.hops else {loc.hops[0].column} @@ -610,7 +649,13 @@ def _is_param_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> boo if g.table != ch.table or d.values.get(p) is None or ch.after is None: continue rel = g.params[p] - if locate(schema, d, _as_change(ch.table, ch.after), d.values[p], max_hops=len(rel.hops)): + if locate( + schema, + d, + _as_change(ch.table, ch.after), + d.values[p], + max_hops=len(rel.hops), + ): return True return False @@ -626,8 +671,14 @@ def _is_ref_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: if not rel.hops or rel.hops[0].table != ch.table or d.values.get(p) is None: continue col = rel.columns[0] if len(rel.columns) == 1 else None - if col and ch.after.get(col) is not None and _rule_ok(ch.after.get(col), d.values[p], rel.rule): - plan.added_refs.setdefault(p, {"table": ch.table, "column": col, "rule": rel.rule}) + if ( + col + and ch.after.get(col) is not None + and _rule_ok(ch.after.get(col), d.values[p], rel.rule) + ): + plan.added_refs.setdefault( + p, {"table": ch.table, "column": col, "rule": rel.rule} + ) return True return False @@ -760,7 +811,9 @@ def empty(self, alias: str, table: str, column: str) -> str: return f"CASE WHEN {ref} IS NULL OR {ref} = '' THEN '1' ELSE '0' END" return f"CASE WHEN {ref} IS NULL THEN '1' ELSE '0' END" - def copied(self, alias: str, detail: Mapping[str, Any], table: str, column: str) -> str: + def copied( + self, alias: str, detail: Mapping[str, Any], table: str, column: str + ) -> str: src = ( f"(SELECT s.{self.q(detail['column'])} FROM {self.q(detail['table'])} s " f"WHERE s.{self.q(detail['ref_column'])} = {alias}.{self.q(detail['via'])})" @@ -819,12 +872,16 @@ class Variant: fields: list[str] lists: dict[str, int] = field(default_factory=dict) dispatch_value: Optional[str] = None - files: dict[str, str] = field(default_factory=dict) # create | found | update -> path + files: dict[str, str] = field( + default_factory=dict + ) # create | found | update -> path sample: dict[str, Any] = field(default_factory=dict) @property def slug(self) -> str: - parts = sorted(self.fields) + [f"{k}x{n}" for k, n in sorted(self.lists.items())] + parts = sorted(self.fields) + [ + f"{k}x{n}" for k, n in sorted(self.lists.items()) + ] if self.dispatch_value is not None: parts.insert(0, f"{self.dispatch_value}") text = "+".join(parts) @@ -872,7 +929,9 @@ def build_contract( named_tables: set[str] = {anchor} # The main record, with every column and the values its parameters reach. - main = Surface("record", settings.views.get(anchor, anchor), _base_columns(b, anchor)) + main = Surface( + "record", settings.views.get(anchor, anchor), _base_columns(b, anchor) + ) match: dict[str, Any] = {} locations = dict(plan.locations) if variant.dispatch_value is not None: @@ -939,7 +998,8 @@ def build_contract( for ip in identity_params: loc = locations[ip] up = Location( - (Hop("fk", anchor, link.column, link.ref_column, True),) + loc.hops, + (Hop("fk", anchor, link.column, link.ref_column, True),) + + loc.hops, loc.columns, loc.rule, ) @@ -947,7 +1007,9 @@ def build_contract( if loc.rule in ("number", "date", "datetime", "minutes"): expr = b.flag(expr, loc.rule, b.ph(ip)) cols.append((f"main__{_alias(ip)}", expr)) - surfaces[sname] = Surface(sname, settings.views.get(g.table, g.table), cols) + surfaces[sname] = Surface( + sname, settings.views.get(g.table, g.table), cols + ) named_tables.add(g.table) surf = surfaces[sname] items = range(variant.lists[p]) if g.list_param else [None] @@ -993,7 +1055,19 @@ def build_contract( for p, ref in plan.added_refs.items(): if p not in fields: continue - effects.append(_raw_effect(b, schema, surfaces, settings, ref["table"], {ref["column"]: _value_expr(p, ref["rule"])}, 1, {}, f"the new {ref['table']} row holds {p}")) + effects.append( + _raw_effect( + b, + schema, + surfaces, + settings, + ref["table"], + {ref["column"]: _value_expr(p, ref["rule"])}, + 1, + {}, + f"the new {ref['table']} row holds {p}", + ) + ) for side in plan.sides: if side.unless is not None and side.unless in fields: continue @@ -1004,7 +1078,17 @@ def build_contract( elif role.role == "actor": m[col] = {"param": role.detail["param"]} effects.append( - _raw_effect(b, schema, surfaces, settings, side.table, m, side.count, side.roles, f"{side.count} row(s) the app adds to {side.table}") + _raw_effect( + b, + schema, + surfaces, + settings, + side.table, + m, + side.count, + side.roles, + f"{side.count} row(s) the app adds to {side.table}", + ) ) verifier: dict[str, Any] = {"kind": "surfaces", "surfaces": {}} @@ -1016,7 +1100,10 @@ def build_contract( verifier["surfaces"][s.name] = sub # Main and child effects read named surfaces: no other read is judged # against them, and the whole-database frame covers every other table. - ignore = sorted({c for cols in bookkeeping.values() for c in cols} & _all_columns(schema, named_tables)) + ignore = sorted( + {c for cols in bookkeeping.values() for c in cols} + & _all_columns(schema, named_tables) + ) frame: dict[str, Any] = {} if ignore: frame["ignore_fields"] = ignore @@ -1056,7 +1143,9 @@ def build_contract( } if nrf: db["new_row_fields"] = nrf - ign = {t: sorted(c) for t, c in bookkeeping.items() if t not in named_tables and c} + ign = { + t: sorted(c) for t, c in bookkeeping.items() if t not in named_tables and c + } if ign: db["ignore_columns"] = ign if settings.recheck_after_s: @@ -1070,9 +1159,15 @@ def build_contract( continue if f"raw_{_alias(t)}" in surfaces: continue - s = Surface(f"raw_{_alias(t)}", settings.views.get(t, t), _base_columns(b, t)) + s = Surface( + f"raw_{_alias(t)}", settings.views.get(t, t), _base_columns(b, t) + ) surfaces[s.name] = s - verifier["surfaces"][s.name] = {"kind": "sql", **settings.read, "sql_query": s.sql(b)} + verifier["surfaces"][s.name] = { + "kind": "sql", + **settings.read, + "sql_query": s.sql(b), + } doc: dict[str, Any] = { "schema_version": "openadapt.verify/v1", "environment_id": settings.environment_id, @@ -1085,13 +1180,19 @@ def build_contract( def _all_columns(schema: Schema, tables: set[str]) -> set[str]: - return {c.name for t in tables if t in schema.tables for c in schema.tables[t].columns} + return { + c.name for t in tables if t in schema.tables for c in schema.tables[t].columns + } def _place(b: SqlBuilder, surf: Surface, table: str, param: str, loc: Location) -> str: """Add the column that holds ``param``'s value to ``surf``; its name.""" - if not loc.hops and len(loc.columns) == 1 and loc.rule in ("exact", "email", "phone"): + if ( + not loc.hops + and len(loc.columns) == 1 + and loc.rule in ("exact", "email", "phone") + ): return loc.columns[0] if loc.columns[0] != "id" else "id" expr = b.path("t", table, loc) alias = f"p__{_alias(param)}" @@ -1158,8 +1259,26 @@ def _raw_effect( for col, value in list(m.items()): c = t.column(col) lit = value.get("literal") if isinstance(value, dict) else None - if c is not None and lit is not None and not ( - c.kind == "text" or (c.kind in ("number", "bool") and c.data_type.startswith(("int", "tinyint", "smallint", "mediumint", "bigint", "integer")) and re.fullmatch(r"-?\d+", lit)) + if ( + c is not None + and lit is not None + and not ( + c.kind == "text" + or ( + c.kind in ("number", "bool") + and c.data_type.startswith( + ( + "int", + "tinyint", + "smallint", + "mediumint", + "bigint", + "integer", + ) + ) + and re.fullmatch(r"-?\d+", lit) + ) + ) ): m.pop(col) nrf = set(getattr(surf, "_new_row_fields", set())) @@ -1169,8 +1288,20 @@ def _raw_effect( role = roles.get(c.name) if role is not None and role.role == "constant" and role.value is None: continue - if role is None or role.role != "constant" or (role.detail or {}).get("is_default"): - if role is None or role.role in ("key", "timestamp", "link", "varies", "copied", "actor", "free"): + if ( + role is None + or role.role != "constant" + or (role.detail or {}).get("is_default") + ): + if role is None or role.role in ( + "key", + "timestamp", + "link", + "varies", + "copied", + "actor", + "free", + ): nrf.add(c.name) continue nrf.add(c.name) @@ -1221,7 +1352,9 @@ def _anchor_roles( # -- variants ----------------------------------------------------------------------- -def variants(schema: Schema, plan: KindPlan, spec: KindSpec, answers: Mapping[str, Any]) -> list[Variant]: +def variants( + schema: Schema, plan: KindPlan, spec: KindSpec, answers: Mapping[str, Any] +) -> list[Variant]: out: list[Variant] = [] lists = list(plan.lists) if plan.mode == "update": @@ -1239,7 +1372,10 @@ def variants(schema: Schema, plan: KindPlan, spec: KindSpec, answers: Mapping[st vp = next(iter(next(iter(values.values())))) for c in schema.tables[plan.anchor].columns: for name in {c.name, _camel(c.name)}: - if name in values or c.name in schema.tables[plan.anchor].primary_key: + if ( + name in values + or c.name in schema.tables[plan.anchor].primary_key + ): continue if c.kind == "text": values[name] = {vp: Location((), (c.name,), "exact")} @@ -1342,7 +1478,11 @@ def dry_read(path: Path, params: Mapping[str, Any], mysql: bool) -> Optional[str would. None when it reads; else why not.""" from openadapt_flow.verifier import Contract, VerifierError - from openadapt_flow.verify_run import FRAME_DB_DETAIL, VerifyRunError, capture_baseline + from openadapt_flow.verify_run import ( + FRAME_DB_DETAIL, + VerifyRunError, + capture_baseline, + ) try: contract = Contract.load(path) @@ -1365,7 +1505,10 @@ def dry_read(path: Path, params: Mapping[str, Any], mysql: bool) -> Optional[str if mysql: snap = detail.get(FRAME_DB_DETAIL) if not isinstance(snap, dict): - from openadapt_flow.runtime.effects.db_snapshot import mac_key, take_snapshot + from openadapt_flow.runtime.effects.db_snapshot import ( + mac_key, + take_snapshot, + ) from openadapt_flow.verify_run import _audit_sql, _db_connect, _ref_columns database = contract.config.frame.database @@ -1420,7 +1563,9 @@ def check_read_only(connect: Any, dialect: str) -> list[str]: "SUPER", "EVENT", ): - if re.search(rf"\b{word}\b", privs) and not privs.startswith("USAGE"): + if re.search(rf"\b{word}\b", privs) and not privs.startswith( + "USAGE" + ): problems.append(f"the read account has {word}: {grant}") else: cur.execute( @@ -1429,7 +1574,9 @@ def check_read_only(connect: Any, dialect: str) -> list[str]: ) row = cur.fetchone() if row and any(row): - problems.append("the read account is a superuser or can create roles or databases") + problems.append( + "the read account is a superuser or can create roles or databases" + ) cur.execute( "SELECT table_name FROM information_schema.role_table_grants " "WHERE grantee = current_user AND privilege_type IN " @@ -1461,7 +1608,9 @@ def _admin_connect(admin_read: Mapping[str, Any]) -> Any: env = admin_read.get("sql_password_env") if env: if env not in os.environ: - raise GenerateError(f"environment variable {env} (the admin password) is not set") + raise GenerateError( + f"environment variable {env} (the admin password) is not set" + ) args["password"] = os.environ[str(env)] return module.connect(**args) @@ -1519,7 +1668,9 @@ def create_views( if schema.dialect == "mysql": cur.execute(f"GRANT SELECT ON {v} TO {reader}") else: - cur.execute(f"GRANT SELECT ON {v} TO {store.quote('postgresql', reader)}") + cur.execute( + f"GRANT SELECT ON {v} TO {store.quote('postgresql', reader)}" + ) made[t] = view conn.commit() finally: @@ -1560,7 +1711,9 @@ def propose( if dialect == "mysql" and not _sees_triggers(connect): if grant_trigger_flag: if not admin_read or not reader_account: - raise GenerateError("--grant-trigger needs --admin-read and --reader-account") + raise GenerateError( + "--grant-trigger needs --admin-read and --reader-account" + ) applied.append(grant_trigger(admin_read, schema, reader_account)) if not _sees_triggers(connect): raise GenerateError( @@ -1599,7 +1752,9 @@ def propose( parts = [] for name, tied in asked.items(): if tied: - opts = "; ".join(f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied)) + opts = "; ".join( + f"[{i}] {loc.describe(p.anchor)}" for i, loc in enumerate(tied) + ) parts.append(f"`{name}` is in several places: {opts}") else: parts.append( @@ -1609,7 +1764,8 @@ def propose( location_questions.append( Question( f"locations.{p.kind}", - f"Kind `{p.kind}`: " + " ".join(parts) + f"Kind `{p.kind}`: " + + " ".join(parts) + f" Answer `locations.{p.kind}: {{: }}` to pick one.", None, ) @@ -1641,26 +1797,35 @@ def propose( lines.append( "Tables treated as the app's own logs (rows may be added, existing " "rows must not change" - + (")" if dialect == "mysql" else "; on PostgreSQL they are not read at all)") + + ( + ")" + if dialect == "mysql" + else "; on PostgreSQL they are not read at all)" + ) + ": " + ", ".join(sorted(logs)) ) if book: lines.append( "Columns that change on their own and are not compared: " - + "; ".join(f"{t}: {', '.join(sorted(c))}" for t, c in sorted(book.items()) if c) + + "; ".join( + f"{t}: {', '.join(sorted(c))}" for t, c in sorted(book.items()) if c + ) ) if unchecked: lines.append( "Columns of a new record that differ between demonstrations and are " "not checked: " - + "; ".join(f"{t}: {', '.join(sorted(c))}" for t, c in sorted(unchecked.items())) + + "; ".join( + f"{t}: {', '.join(sorted(c))}" for t, c in sorted(unchecked.items()) + ) ) questions.append( Question( "bookkeeping", "Is this the app's own bookkeeping, which a write may change without " - "a task asking? " + " ".join(lines) + "a task asking? " + + " ".join(lines) + " Answer `bookkeeping: accept`, or list changes under " "logs_add, logs_remove, columns_add, columns_remove.", "accept", @@ -1682,10 +1847,13 @@ def propose( ) ) questions.extend(location_questions) - questions_open = [ - q for q in questions if _answer(answers, q.key) is None - ] - settings = Settings(read, dict(actor_params), recheck or (DEFAULT_RECHECK_S if dialect == "mysql" else None), env) + questions_open = [q for q in questions if _answer(answers, q.key) is None] + settings = Settings( + read, + dict(actor_params), + recheck or (DEFAULT_RECHECK_S if dialect == "mysql" else None), + env, + ) # Tables whose name the SQL guard refuses: a view, or a refusal. blocked = sorted( @@ -1695,14 +1863,21 @@ def propose( ) needed = sorted( {p.anchor for p in plans if p.anchor in blocked} - | {g.table for p in plans for g in list(p.children.values()) + list(p.lists.values()) if g.table in blocked} + | { + g.table + for p in plans + for g in list(p.children.values()) + list(p.lists.values()) + if g.table in blocked + } ) if needed: existing = {t: f"oa_read_{t}" for t in needed if f"oa_read_{t}" in schema.views} missing = [t for t in needed if t not in existing] if missing and create_views_flag: if not admin_read or not reader_account: - raise GenerateError("--create-views needs --admin-read and --reader-account") + raise GenerateError( + "--create-views needs --admin-read and --reader-account" + ) existing.update(create_views(admin_read, schema, missing, reader_account)) schema = store.read_schema(connect, dialect) for t, v in list(existing.items()): @@ -1710,7 +1885,9 @@ def propose( existing.pop(t) settings.views = existing for p in plans: - tables = {p.anchor} | {g.table for g in list(p.children.values()) + list(p.lists.values())} + tables = {p.anchor} | { + g.table for g in list(p.children.values()) + list(p.lists.values()) + } for t in sorted(tables & set(blocked)): if t not in existing: p.refused.append( @@ -1733,7 +1910,13 @@ def propose( "dialect": dialect, "tables": len(schema.tables), "questions": [ - {"number": i + 1, "key": q.key, "text": q.text, "proposal": q.proposal, "answered": _answer(answers, q.key) is not None} + { + "number": i + 1, + "key": q.key, + "text": q.text, + "proposal": q.proposal, + "answered": _answer(answers, q.key) is not None, + } for i, q in enumerate(questions) ], "kinds": {}, @@ -1751,15 +1934,23 @@ def propose( rep: dict[str, Any] = { "mode": p.mode, "main_table": p.anchor, - "locations": {k: v.describe(p.anchor) for k, v in sorted(p.locations.items())}, + "locations": { + k: v.describe(p.anchor) for k, v in sorted(p.locations.items()) + }, "placed_by_name": sorted(p.by_name), "demonstrations": len(p.demos), "children": {k: g.table for k, g in p.children.items()}, "lists": {k: g.table for k, g in p.lists.items()}, - "side_rows": [{"table": s.table, "count": s.count, "unless": s.unless} for s in p.sides], + "side_rows": [ + {"table": s.table, "count": s.count, "unless": s.unless} + for s in p.sides + ], "logs": sorted(p.logs), "refused": list(p.refused), - "unresolved": {k: [loc.describe(p.anchor) for loc in v] for k, v in sorted(p.unresolved.items())}, + "unresolved": { + k: [loc.describe(p.anchor) for loc in v] + for k, v in sorted(p.unresolved.items()) + }, "emitted": 0, "rejected": [], } @@ -1774,7 +1965,13 @@ def propose( continue flavors = ["create", "found"] if p.mode == "create" else ["update"] for v in variants(schema, p, spec, answers): - if any(f not in p.locations and f != p.dispatch and f not in p.children and f not in (p.dispatch_locations.get(v.dispatch_value or "", {})) for f in v.fields): + if any( + f not in p.locations + and f != p.dispatch + and f not in p.children + and f not in (p.dispatch_locations.get(v.dispatch_value or "", {})) + for f in v.fields + ): continue ok = True for flavor in flavors: @@ -1787,15 +1984,25 @@ def propose( f"{flavor}, fields {', '.join(v.fields + [f'{k}x{n}' for k, n in v.lists.items()])}.\n" "# Proposed, not admitted: a person answers QUESTIONS.md first.\n" ) - path.write_text(header + yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=100), encoding="utf-8") + path.write_text( + header + + yaml.safe_dump( + doc, sort_keys=False, allow_unicode=True, width=100 + ), + encoding="utf-8", + ) why = None for s in doc["verifier"]["surfaces"].values(): why = why or guard_problem(s["sql_query"]) if why is None and validate: - why = dry_read(path, flatten(sample_values(p, v)), dialect == "mysql") + why = dry_read( + path, flatten(sample_values(p, v)), dialect == "mysql" + ) if why is not None: path.unlink() - rep["rejected"].append({"variant": v.slug, "flavor": flavor, "why": why}) + rep["rejected"].append( + {"variant": v.slug, "flavor": flavor, "why": why} + ) ok = False break v.files[flavor] = str(rel) @@ -1814,27 +2021,41 @@ def propose( (out / f).unlink(missing_ok=True) manifest["kinds"][p.kind] = entry report["kinds"][p.kind] = rep - (out / "manifest.yaml").write_text(yaml.safe_dump(manifest, sort_keys=False, allow_unicode=True), encoding="utf-8") - (out / "report.json").write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8") - (out / "QUESTIONS.md").write_text(_questions_md(questions, answers, report), encoding="utf-8") + (out / "manifest.yaml").write_text( + yaml.safe_dump(manifest, sort_keys=False, allow_unicode=True), encoding="utf-8" + ) + (out / "report.json").write_text( + json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8" + ) + (out / "QUESTIONS.md").write_text( + _questions_md(questions, answers, report), encoding="utf-8" + ) report["questions_open"] = [q.key for q in questions_open] return report -def _questions_md(questions: Sequence[Question], answers: Mapping[str, Any], report: Mapping[str, Any]) -> str: +def _questions_md( + questions: Sequence[Question], answers: Mapping[str, Any], report: Mapping[str, Any] +) -> str: lines = ["# Questions for a person", ""] if not questions: - lines.append("None: every decision followed from the schema and the demonstrations.") + lines.append( + "None: every decision followed from the schema and the demonstrations." + ) for i, q in enumerate(questions, 1): done = _answer(answers, q.key) lines.append(f"{i}. {q.text}") lines.append(f" - Answer key: `{q.key}`. Proposed: `{q.proposal}`.") - lines.append(f" - {'Answered: `' + json.dumps(done, ensure_ascii=False) + '`' if done is not None else 'Not answered yet.'}") + lines.append( + f" - {'Answered: `' + json.dumps(done, ensure_ascii=False) + '`' if done is not None else 'Not answered yet.'}" + ) lines.append("") lines.append("## Kinds") lines.append("") for kind, rep in report["kinds"].items(): - lines.append(f"- `{kind}` ({rep['mode'] or 'no contract'}, main table `{rep['main_table']}`): {rep['emitted']} contract(s).") + lines.append( + f"- `{kind}` ({rep['mode'] or 'no contract'}, main table `{rep['main_table']}`): {rep['emitted']} contract(s)." + ) if rep.get("demonstrations") == 1 and rep["mode"] == "create": lines.append( " - One demonstration only: every column of a new record that the task " @@ -1927,7 +2148,11 @@ def _exists(contract: Any, params: Mapping[str, Any]) -> bool: def _now() -> str: - return _dt.datetime.now(_dt.timezone.utc).replace(tzinfo=None).isoformat(timespec="seconds") + return ( + _dt.datetime.now(_dt.timezone.utc) + .replace(tzinfo=None) + .isoformat(timespec="seconds") + ) def _parse_actor(items: Sequence[str]) -> dict[str, str]: @@ -1954,32 +2179,84 @@ def main(argv: Optional[Sequence[str]] = None) -> int: ), ) sub = ap.add_subparsers(dest="command", required=True) - s = sub.add_parser("schema", help="read the schema and check the account is read-only") - s.add_argument("--read", required=True, type=Path, help="read.yaml: sql_driver, sql_connect_args, sql_password_env") + s = sub.add_parser( + "schema", help="read the schema and check the account is read-only" + ) + s.add_argument( + "--read", + required=True, + type=Path, + help="read.yaml: sql_driver, sql_connect_args, sql_password_env", + ) s.add_argument("--out", type=Path, help="write schema.json here") - s = sub.add_parser("snapshot", help="snapshot every table before a demonstration write") + s = sub.add_parser( + "snapshot", help="snapshot every table before a demonstration write" + ) s.add_argument("--read", required=True, type=Path) - s.add_argument("--out", required=True, type=Path, help="e.g. work/before.json (holds row values: keep it private)") - s = sub.add_parser("record", help="snapshot again after the write and save the demonstration") + s.add_argument( + "--out", + required=True, + type=Path, + help="e.g. work/before.json (holds row values: keep it private)", + ) + s = sub.add_parser( + "record", help="snapshot again after the write and save the demonstration" + ) s.add_argument("--read", required=True, type=Path) s.add_argument("--before", required=True, type=Path) s.add_argument("--kind", required=True) - s.add_argument("--values", required=True, help="the task's values as JSON, or @file.json") - s.add_argument("--demos", required=True, type=Path, help="directory of demonstrations") - s.add_argument("--name", default="", help="file name stem (default: kind and a number)") + s.add_argument( + "--values", required=True, help="the task's values as JSON, or @file.json" + ) + s.add_argument( + "--demos", required=True, type=Path, help="directory of demonstrations" + ) + s.add_argument( + "--name", default="", help="file name stem (default: kind and a number)" + ) s = sub.add_parser("propose", help="analyze the demonstrations and write contracts") s.add_argument("--read", required=True, type=Path) - s.add_argument("--kinds", required=True, type=Path, help="kinds.yaml: kinds, parameters, examples") + s.add_argument( + "--kinds", + required=True, + type=Path, + help="kinds.yaml: kinds, parameters, examples", + ) s.add_argument("--demos", required=True, type=Path) s.add_argument("--out", required=True, type=Path) - s.add_argument("--answers", type=Path, help="answers.yaml with the person's answers") - s.add_argument("--actor-param", action="append", default=[], help="NAME=VALUE the caller passes for the acting user (e.g. bot_user_id=5); a column holding it is the actor") + s.add_argument( + "--answers", type=Path, help="answers.yaml with the person's answers" + ) + s.add_argument( + "--actor-param", + action="append", + default=[], + help="NAME=VALUE the caller passes for the acting user (e.g. bot_user_id=5); a column holding it is the actor", + ) s.add_argument("--environment-id", default="") - s.add_argument("--create-views", action="store_true", help="create a view for a table the SQL guard refuses by name (needs --admin-read)") - s.add_argument("--admin-read", type=Path, help="admin.yaml: a connection allowed to CREATE VIEW and GRANT") - s.add_argument("--reader-account", default="", help="the read account to GRANT SELECT on created views (MySQL: 'user'@'%%')") - s.add_argument("--grant-trigger", action="store_true", help="MySQL: GRANT TRIGGER to the read account through --admin-read, so the frame can see triggers") - s.add_argument("--no-validate", action="store_true", help="skip the dry read (tests only)") + s.add_argument( + "--create-views", + action="store_true", + help="create a view for a table the SQL guard refuses by name (needs --admin-read)", + ) + s.add_argument( + "--admin-read", + type=Path, + help="admin.yaml: a connection allowed to CREATE VIEW and GRANT", + ) + s.add_argument( + "--reader-account", + default="", + help="the read account to GRANT SELECT on created views (MySQL: 'user'@'%%')", + ) + s.add_argument( + "--grant-trigger", + action="store_true", + help="MySQL: GRANT TRIGGER to the read account through --admin-read, so the frame can see triggers", + ) + s.add_argument( + "--no-validate", action="store_true", help="skip the dry read (tests only)" + ) args = ap.parse_args(argv) try: read = load_read(args.read) @@ -1989,8 +2266,12 @@ def main(argv: Optional[Sequence[str]] = None) -> int: schema = store.read_schema(connect, dialect) problems = check_read_only(connect, dialect) if args.out: - args.out.write_text(json.dumps(schema.to_json(), indent=1), encoding="utf-8") - print(f"{dialect} {schema.database}: {len(schema.tables)} tables, {len(schema.views)} views") + args.out.write_text( + json.dumps(schema.to_json(), indent=1), encoding="utf-8" + ) + print( + f"{dialect} {schema.database}: {len(schema.tables)} tables, {len(schema.views)} views" + ) for p in problems: print(f"NOT READ-ONLY: {p}") return 1 if problems else 0 @@ -1998,7 +2279,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int: schema = store.read_schema(connect, dialect) rows = store.snapshot(connect, schema) store.save_snapshot(args.out, rows) - (args.out.with_suffix(".meta.json")).write_text(json.dumps({"taken_at": _now()}), encoding="utf-8") + (args.out.with_suffix(".meta.json")).write_text( + json.dumps({"taken_at": _now()}), encoding="utf-8" + ) print(f"snapshot of {len(rows)} tables -> {args.out}") return 0 if args.command == "record": @@ -2026,9 +2309,13 @@ def main(argv: Optional[Sequence[str]] = None) -> int: "diff": d.to_json(), } path = args.demos / f"{stem}.demo.json" - path.write_text(json.dumps(doc, indent=1, ensure_ascii=False), encoding="utf-8") + path.write_text( + json.dumps(doc, indent=1, ensure_ascii=False), encoding="utf-8" + ) os.chmod(path, 0o600) - print(f"{path}: {len(d.changes)} row change(s) in {', '.join(d.tables()) or 'no table'}") + print( + f"{path}: {len(d.changes)} row change(s) in {', '.join(d.tables()) or 'no table'}" + ) return 0 answers = load_yaml(args.answers) if args.answers else {} admin = load_read(args.admin_read) if args.admin_read else None @@ -2046,9 +2333,15 @@ def main(argv: Optional[Sequence[str]] = None) -> int: validate=not args.no_validate, grant_trigger_flag=args.grant_trigger, ) - print(f"questions: {len(report['questions'])} ({len(report['questions_open'])} open) -> {args.out / 'QUESTIONS.md'}") + print( + f"questions: {len(report['questions'])} ({len(report['questions_open'])} open) -> {args.out / 'QUESTIONS.md'}" + ) for kind, rep in report["kinds"].items(): - status = "; ".join(rep["refused"]) if rep["refused"] else f"{rep['emitted']} contract(s)" + status = ( + "; ".join(rep["refused"]) + if rep["refused"] + else f"{rep['emitted']} contract(s)" + ) print(f" {kind}: {status}") return 0 except (GenerateError, store.StoreError) as exc: diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index 1e5b566bc1..aab132a904 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -102,7 +102,9 @@ def __init__(self, port: int, db: str) -> None: self.port, self.db = port, db def con(self) -> Any: - c = psycopg2.connect(host="127.0.0.1", port=self.port, user="admin", dbname=self.db) + c = psycopg2.connect( + host="127.0.0.1", port=self.port, user="admin", dbname=self.db + ) c.autocommit = True return c @@ -118,10 +120,14 @@ def one(self, sql: str, args: tuple = ()) -> Any: def user(self, name: str) -> int: first, last = name.split(" ", 1) - return self.one("SELECT id FROM users WHERE firstname=%s AND lastname=%s", (first, last)) + return self.one( + "SELECT id FROM users WHERE firstname=%s AND lastname=%s", (first, last) + ) def create_issue(self, v: dict[str, Any], *, project_id: Any = None) -> int: - pid = project_id or self.one("SELECT id FROM projects WHERE name=%s", (v["project"],)) + pid = project_id or self.one( + "SELECT id FROM projects WHERE name=%s", (v["project"],) + ) sid = self.one("SELECT id FROM statuses WHERE name=%s", (v["status"],)) uid = self.user(v["assignee"]) if v.get("assignee") else None iid = self.one( @@ -142,7 +148,12 @@ def find(self, project: str, subject: str) -> int: def update_issue(self, iid: int, v: dict[str, Any], *, user: int = BOT) -> None: sets, details = [], [] if v.get("status") is not None: - sets.append(("status_id", self.one("SELECT id FROM statuses WHERE name=%s", (v["status"],)))) + sets.append( + ( + "status_id", + self.one("SELECT id FROM statuses WHERE name=%s", (v["status"],)), + ) + ) if v.get("assignee") is not None: sets.append(("assigned_to_id", self.user(v["assignee"]))) if v.get("due_date") is not None: @@ -153,7 +164,10 @@ def update_issue(self, iid: int, v: dict[str, Any], *, user: int = BOT) -> None: old = self.one(f"SELECT {col}::text FROM issues WHERE id=%s", (iid,)) self.q(f"UPDATE issues SET {col}=%s WHERE id=%s", (val, iid)) details.append((col, old, str(val))) - self.q("UPDATE issues SET updated_on=now(), lock_version=lock_version+1 WHERE id=%s", (iid,)) + self.q( + "UPDATE issues SET updated_on=now(), lock_version=lock_version+1 WHERE id=%s", + (iid,), + ) jid = self.one( "INSERT INTO journals (journalized_id, journalized_type, user_id, notes, created_on) " "VALUES (%s,'Issue',%s,%s,now()) RETURNING id", (iid, user, v.get("notes") or "")) # fmt: skip @@ -166,23 +180,71 @@ def update_issue(self, iid: int, v: dict[str, Any], *, user: int = BOT) -> None: "kinds": [ { "kind": "create_issue", - "parameters": ["project", "subject", "status", "assignee", "due_date", "customer_ref"], + "parameters": [ + "project", + "subject", + "status", + "assignee", + "due_date", + "customer_ref", + ], "examples": [ - {"values": {"project": "Billing Portal", "subject": "Tax rounding", "status": "New", - "assignee": "Jon Park", "due_date": "2026-11-20", "customer_ref": None}}, - {"values": {"project": "Help Desk", "subject": "Onboard Zoë", "status": "New", - "assignee": "Renée O'Connor", "due_date": None, "customer_ref": "007341"}}, + { + "values": { + "project": "Billing Portal", + "subject": "Tax rounding", + "status": "New", + "assignee": "Jon Park", + "due_date": "2026-11-20", + "customer_ref": None, + } + }, + { + "values": { + "project": "Help Desk", + "subject": "Onboard Zoë", + "status": "New", + "assignee": "Renée O'Connor", + "due_date": None, + "customer_ref": "007341", + } + }, ], }, { "kind": "update_issue", - "parameters": ["project", "subject", "status", "assignee", "due_date", "done_ratio", "notes"], + "parameters": [ + "project", + "subject", + "status", + "assignee", + "due_date", + "done_ratio", + "notes", + ], "examples": [ - {"values": {"project": "Help Desk", "subject": "Printer jams", "status": "In Progress", - "assignee": "Jon Park", "due_date": None, "done_ratio": None, "notes": None}}, - {"values": {"project": "Billing Portal", "subject": "Refund button missing", - "status": None, "assignee": None, "due_date": None, "done_ratio": 30, - "notes": "Customer confirmed the refund."}}, + { + "values": { + "project": "Help Desk", + "subject": "Printer jams", + "status": "In Progress", + "assignee": "Jon Park", + "due_date": None, + "done_ratio": None, + "notes": None, + } + }, + { + "values": { + "project": "Billing Portal", + "subject": "Refund button missing", + "status": None, + "assignee": None, + "due_date": None, + "done_ratio": 30, + "notes": "Customer confirmed the refund.", + } + }, ], }, ] @@ -192,7 +254,9 @@ def update_issue(self, iid: int, v: dict[str, Any], *, user: int = BOT) -> None: @pytest.fixture(scope="module") def generated(pg: Any) -> dict[str, Any]: root, port = pg["root"], pg["port"] - admin = psycopg2.connect(host="127.0.0.1", port=port, user="admin", dbname="postgres") + admin = psycopg2.connect( + host="127.0.0.1", port=port, user="admin", dbname="postgres" + ) admin.autocommit = True admin.cursor().execute("CREATE DATABASE tracker") admin.close() @@ -220,7 +284,11 @@ def demo(kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: demo("create_issue", ex["values"], lambda v=ex["values"]: app.create_issue(v)) for ex in KINDS["kinds"][1]["examples"]: v = ex["values"] - demo("update_issue", v, lambda v=v: app.update_issue(app.find(v["project"], v["subject"]), v)) + demo( + "update_issue", + v, + lambda v=v: app.update_issue(app.find(v["project"], v["subject"]), v), + ) argv = ["propose", *common, "--kinds", str(work / "kinds.yaml"), "--demos", str(work / "demos"), "--out", str(work / "out"), "--actor-param", f"bot_user_id={BOT}"] # fmt: skip assert gen.main(argv) == 0 @@ -234,16 +302,27 @@ def demo(kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: return {"app": app, "out": work / "out", "report": report, "read": read} -def _run(generated: dict[str, Any], kind: str, values: dict[str, Any], write: Callable[[], Any]) -> str: +def _run( + generated: dict[str, Any], + kind: str, + values: dict[str, Any], + write: Callable[[], Any], +) -> str: from openadapt_flow.verifier import Actor, begin_write, verify_write - contract, params = gen.choose(generated["out"] / "manifest.yaml", kind, values, {"bot_user_id": str(BOT)}) + contract, params = gen.choose( + generated["out"] / "manifest.yaml", kind, values, {"bot_user_id": str(BOT)} + ) with tempfile.TemporaryDirectory() as tmp: pending = begin_write(contract, params, actor=Actor("agent", "ops-bot"), ledger=None, write_key=f"t-{time.time_ns()}", workdir=tmp) # fmt: skip write() result = verify_write(pending) - return getattr(result.outcome, "value", str(result.outcome)) + " " + "; ".join(result.reasons)[:400] + return ( + getattr(result.outcome, "value", str(result.outcome)) + + " " + + "; ".join(result.reasons)[:400] + ) def test_questions_and_contracts(generated: dict[str, Any]) -> None: @@ -257,7 +336,9 @@ def test_questions_and_contracts(generated: dict[str, Any]) -> None: # Four change fields the examples give (status, assignee, done ratio, # notes): every non-empty subset gets a contract. No example gives a due # date, so no contract changes one. - assert kinds["update_issue"]["emitted"] == 15, json.dumps(kinds["update_issue"], indent=1) + assert kinds["update_issue"]["emitted"] == 15, json.dumps( + kinds["update_issue"], indent=1 + ) assert "journal_details" in kinds["update_issue"]["logs"] @@ -265,7 +346,9 @@ def test_a_correct_create_is_confirmed(generated: dict[str, Any]) -> None: app = generated["app"] v = {"project": "Billing Portal", "subject": "Dark mode", "status": "New", "assignee": "Renée O'Connor", "due_date": "2026-12-01", "customer_ref": "0099"} # fmt: skip - assert _run(generated, "create_issue", v, lambda: app.create_issue(v)).startswith("CONFIRMED") + assert _run(generated, "create_issue", v, lambda: app.create_issue(v)).startswith( + "CONFIRMED" + ) def test_found_existing_with_no_write_is_confirmed(generated: dict[str, Any]) -> None: @@ -276,12 +359,16 @@ def test_found_existing_with_no_write_is_confirmed(generated: dict[str, Any]) -> assert _run(generated, "create_issue", v, lambda: None).startswith("CONFIRMED") -def test_an_undemonstrated_update_combination_is_confirmed(generated: dict[str, Any]) -> None: +def test_an_undemonstrated_update_combination_is_confirmed( + generated: dict[str, Any], +) -> None: app = generated["app"] v = {"project": "Help Desk", "subject": "VPN drops", "status": "Resolved", "assignee": None, "due_date": None, "done_ratio": 80, "notes": "Fixed the tunnel."} # fmt: skip iid = app.find("Help Desk", "VPN drops") - assert _run(generated, "update_issue", v, lambda: app.update_issue(iid, v)).startswith("CONFIRMED") + assert _run( + generated, "update_issue", v, lambda: app.update_issue(iid, v) + ).startswith("CONFIRMED") CREATE = {"project": "Billing Portal", "subject": "Export CSV", "status": "New", @@ -332,4 +419,6 @@ def test_each_fault_is_stopped(generated: dict[str, Any], name: str) -> None: for t, rows in snapshot.items(): cur.execute(f"DELETE FROM {t}") for r in rows: - cur.execute(f"INSERT INTO {t} VALUES ({', '.join(['%s'] * len(r))})", r) + cur.execute( + f"INSERT INTO {t} VALUES ({', '.join(['%s'] * len(r))})", r + ) From ac6ad51941e217270ce1a50cd9b09f47f88ecc73 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:05:39 -0400 Subject: [PATCH 47/94] WIP: found flavor for updates already in place --- packages/flow/docs/GENERATE_CONTRACTS.md | 17 ++++++++++++----- .../openadapt_flow/generate/verify_contract.py | 8 ++++---- .../flow/tests/test_generate_verify_contract.py | 17 ++++++++++++++++- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 53aab255ce..821f0597cb 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -83,8 +83,8 @@ server, use `openadapt_flow.generate.ensure_contract` (an ## What it writes - `contracts//..yaml`: one contract per field set. The - flavor is `create` and `found` for a kind that adds a record, `update` for a - kind that changes one. + flavor is `create` or `update`, and each has a `found` twin for a task + whose record already holds its values. - `manifest.yaml`: which contract fits which task values. - `QUESTIONS.md`: the numbered questions, the proposed answers, and per kind how many contracts were written and why any were not. @@ -149,9 +149,10 @@ gets a contract per length, from 1 to 10 items. - `create`: exactly one new main record holds the task's values, and everything else in the new row is what the demonstrations showed, or is bookkeeping. -- `found`: the record already exists, and nothing in the database changed. - `choose` reads the store first and picks `found` when a record with the - task's values is already there. +- `found`: the record already holds the task's values, and nothing in the + database changed. `choose` reads the store first and picks `found` when a + record with the task's values is already there: a "make sure it exists" + task whose record exists, or an update that is already in place. - `update`: exactly one record matches the task's identity values and holds the new values. Only the fields the task changes may change. @@ -207,6 +208,12 @@ why. - Columns of a new record that differ between demonstrations and aren't task values (an invoice's computed totals, its draft reference) are not checked. The bookkeeping question lists them. +- Free-text columns of a new record that the task doesn't give, and that + every demonstration left empty (a line description, a note), are not + checked: an agent may fill them from the record it copies. +- With one demonstration of a create, every other column must hold the value + it held then, including one computed from the task's values (a price with + tax). `QUESTIONS.md` says so; record a second create with other values. - On PostgreSQL there is no whole-database frame and no recheck: tables proposed as logs are not read, and a write that lands after the check is not seen. diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 0342476c1e..58632ec7ab 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1963,7 +1963,7 @@ def propose( manifest["kinds"][p.kind] = entry report["kinds"][p.kind] = rep continue - flavors = ["create", "found"] if p.mode == "create" else ["update"] + flavors = ["create", "found"] if p.mode == "create" else ["update", "found"] for v in variants(schema, p, spec, answers): if any( f not in p.locations @@ -2115,12 +2115,12 @@ def choose( continue params = {**flatten(values), **dict(extra_params or {})} base = manifest_path.parent - if entry["mode"] == "update": - return Contract.load(base / var["update"]), params + # The record already holds the task's values (found, or an update + # already in place): nothing may change. Else the create or update. found = Contract.load(base / var["found"]) if _exists(found, params): return found, params - return Contract.load(base / var["create"]), params + return Contract.load(base / var[entry["mode"]]), params raise GenerateError( f"no {kind} contract for the fields {sorted(given)}" + (f" and lists {lists}" if lists else "") diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index aab132a904..d3cf81c4b4 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -371,7 +371,22 @@ def test_an_undemonstrated_update_combination_is_confirmed( ).startswith("CONFIRMED") -CREATE = {"project": "Billing Portal", "subject": "Export CSV", "status": "New", +def test_an_update_already_in_place_with_no_write_is_confirmed( + generated: dict[str, Any], +) -> None: + app = generated["app"] + v = {"project": "Help Desk", "subject": "VPN drops", "status": None, "assignee": None, + "due_date": None, "done_ratio": 80, "notes": None} # fmt: skip + app.q("UPDATE issues SET done_ratio=80 WHERE id=%s", (app.find("Help Desk", "VPN drops"),)) + assert _run(generated, "update_issue", v, lambda: None).startswith("CONFIRMED") + # The same task with a stray write on the issue is stopped. + stray = _run(generated, "update_issue", v, + lambda: app.q("UPDATE issues SET is_private=true WHERE subject='VPN drops'")) # fmt: skip + app.q("UPDATE issues SET is_private=false WHERE subject='VPN drops'") + assert not stray.startswith("CONFIRMED"), stray + + +CREATE ={"project": "Billing Portal", "subject": "Export CSV", "status": "New", "assignee": "Jon Park", "due_date": "2026-12-05", "customer_ref": "0042"} # fmt: skip UPDATE = {"project": "Billing Portal", "subject": "Refund button missing", "status": "Resolved", "assignee": None, "due_date": None, "done_ratio": None, "notes": "Done."} # fmt: skip From 03bfb9f83b5a337c3f562c39673488b9a42ebd25 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:06:31 -0400 Subject: [PATCH 48/94] WIP: validation runs the declared recheck on late-write faults --- .../flow/benchmark/generate_contracts/summarize.py | 3 ++- .../flow/benchmark/generate_contracts/validate.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/flow/benchmark/generate_contracts/summarize.py b/packages/flow/benchmark/generate_contracts/summarize.py index efe88d438c..a277508deb 100644 --- a/packages/flow/benchmark/generate_contracts/summarize.py +++ b/packages/flow/benchmark/generate_contracts/summarize.py @@ -41,7 +41,8 @@ def main() -> None: misses, false_stops = [], [] for r in rows: k = r["kind"] - ok = r["verdict"]["outcome"] == "CONFIRMED" + # A recheck that refutes a confirmed write turns it into a stop (late). + ok = r["verdict"]["outcome"] == "CONFIRMED" and r.get("recheck", {}).get("outcome", "CONFIRMED") == "CONFIRMED" label = str(r["oracle"].get("label")) harm = label not in GOOD sc = scope.get(r["case"].split("-", 1)[1], "in") diff --git a/packages/flow/benchmark/generate_contracts/validate.py b/packages/flow/benchmark/generate_contracts/validate.py index d7371955bb..6e2b4f631d 100644 --- a/packages/flow/benchmark/generate_contracts/validate.py +++ b/packages/flow/benchmark/generate_contracts/validate.py @@ -26,7 +26,7 @@ from demo_lib import FCG, fixture, load from openadapt_flow.generate import verify_contract as gen -from openadapt_flow.verifier import Actor, begin_write, verify_write +from openadapt_flow.verifier import Actor, begin_write, recheck, verify_write LATE_WAIT_S = 150 @@ -128,6 +128,18 @@ def main() -> int: time.sleep(late) if hasattr(fx, "wait_late_writes"): fx.wait_late_writes() + if rec["verdict"]["outcome"] == "CONFIRMED" and rec["verdict"].get("recheck_due_at"): + # A write that lands after the check: the recheck the contract + # declares (frame.database.recheck_after_s) reads it. + try: + late_r = recheck(contract, r, params=params, + actor=Actor("agent", "generated-check"), wait=True) # fmt: skip + rec["recheck"] = { + "outcome": getattr(late_r.outcome, "value", str(late_r.outcome)), + "reasons": list(late_r.reasons), + } + except Exception as exc: # noqa: BLE001 + rec["recheck"] = {"outcome": "RECHECK_CRASH", "reasons": [f"{type(exc).__name__}: {exc}"]} after = oracle.snapshot() try: rec["oracle"] = oracle.judge(task, before, after) From 1b8437ce6c5e0608a07615fa9135f11668adf880 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:28:48 -0400 Subject: [PATCH 49/94] WIP: side rows in the bookkeeping question; Dolibarr results --- CAPABILITIES.md | 1 + .../results/dolibarr/QUESTIONS.md | 20 + .../results/dolibarr/answers.yaml | 3 + ...l+name+phone+town+zip.8386ba67.create.yaml | 352 +++++++++++++ ...il+name+phone+town+zip.8386ba67.found.yaml | 106 ++++ ...ice_date+town+linesx2.17de9df9.create.yaml | 496 ++++++++++++++++++ ...oice_date+town+linesx2.17de9df9.found.yaml | 93 ++++ ...customer+town+linesx2.c91f9518.create.yaml | 484 +++++++++++++++++ .../customer+town+linesx2.c91f9518.found.yaml | 88 ++++ ...excl_vat+ref+vat_rate.bcd72fd0.create.yaml | 426 +++++++++++++++ ..._excl_vat+ref+vat_rate.bcd72fd0.found.yaml | 104 ++++ ...w_email+new_phone+town.be743cce.found.yaml | 101 ++++ ..._email+new_phone+town.be743cce.update.yaml | 218 ++++++++ ...w_email+new_phone+town.110f7554.found.yaml | 99 ++++ ..._email+new_phone+town.110f7554.update.yaml | 215 ++++++++ .../results/dolibarr/manifest.yaml | 311 +++++++++++ .../results/dolibarr/report.json | 157 ++++++ .../generate/verify_contract.py | 15 +- 18 files changed, 3288 insertions(+), 1 deletion(-) create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/report.json diff --git a/CAPABILITIES.md b/CAPABILITIES.md index b8206ee3cf..f9dea9d75e 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,6 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, and a recheck window the person confirms. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies: Dolibarr 3 questions; RESULTS_PLACEHOLDER) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md new file mode 100644 index 0000000000..92a0749f46 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md @@ -0,0 +1,20 @@ +# Questions for a person + +1. Is this the app's own bookkeeping, which a write may change without a task asking? Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. + - Answer key: `bookkeeping`. Proposed: `accept`. + - Answered: `"accept"` + +2. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. + - Answer key: `recheck_after_s`. Proposed: `300`. + - Answered: `300` + +3. Kind `create_draft_invoice`: `invoice_date` is in several places: [0] llx_facture.datef (date); [1] llx_facture.date_lim_reglement (date) Answer `locations.create_draft_invoice: {: }` to pick one. + - Answer key: `locations.create_draft_invoice`. Proposed: `None`. + - Answered: `{"invoice_date": 0}` + +## Kinds + +- `create_customer` (create, main table `llx_societe`): 1 contract(s). +- `create_product` (create, main table `llx_product`): 1 contract(s). +- `create_draft_invoice` (create, main table `llx_facture`): 10 contract(s). +- `update_customer` (update, main table `llx_societe`): 15 contract(s). diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml new file mode 100644 index 0000000000..d81a017c37 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml @@ -0,0 +1,3 @@ +bookkeeping: accept +recheck_after_s: 300 +locations.create_draft_invoice: {invoice_date: 0} diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml new file mode 100644 index 0000000000..095d9bcd5b --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml @@ -0,0 +1,352 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_customer, create, fields address, country, email, name, phone, town, zip. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key`, (SELECT CAST(a1.`label` + AS CHAR) FROM `llx_c_country` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_pays` AS BINARY)) + AS `p__country`, CASE WHEN t.`capital` IS NULL THEN '1' ELSE '0' END AS `pin__capital`, CASE WHEN + t.`client` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__client`, CASE WHEN t.`cond_reglement` + IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement`, CASE WHEN t.`cond_reglement_supplier` + IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement_supplier`, CASE WHEN t.`customer_bad` = + CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_bad`, CASE WHEN t.`customer_rate` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_rate`, CASE WHEN t.`entity` + = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fk_account` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_barcode_type` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, CASE WHEN t.`fk_departement` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_departement`, CASE WHEN t.`fk_effectif` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_effectif`, CASE WHEN t.`fk_forme_juridique` IS NULL THEN '1' ELSE + '0' END AS `pin__fk_forme_juridique`, CASE WHEN t.`fk_incoterms` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_incoterms`, CASE WHEN t.`fk_multicurrency` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_shipping_method` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_shipping_method`, CASE WHEN t.`fk_stcomm` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_stcomm`, CASE WHEN t.`fk_typent` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_typent`, CASE WHEN t.`fk_warehouse` IS NULL THEN '1' ELSE '0' + END AS `pin__fk_warehouse`, CASE WHEN t.`fournisseur` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE + '0' END AS `pin__fournisseur`, CASE WHEN t.`geolat` IS NULL THEN '1' ELSE '0' END AS `pin__geolat`, + CASE WHEN t.`geolong` IS NULL THEN '1' ELSE '0' END AS `pin__geolong`, CASE WHEN t.`geopoint` + IS NULL THEN '1' ELSE '0' END AS `pin__geopoint`, CASE WHEN t.`localtax1_assuj` = CAST('0' AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_assuj`, CASE WHEN t.`localtax1_value` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_value`, CASE WHEN t.`localtax2_assuj` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_assuj`, CASE WHEN t.`localtax2_value` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_value`, CASE WHEN t.`mode_reglement` + IS NULL THEN '1' ELSE '0' END AS `pin__mode_reglement`, CASE WHEN t.`mode_reglement_supplier` + IS NULL THEN '1' ELSE '0' END AS `pin__mode_reglement_supplier`, CASE WHEN t.`order_min_amount` + IS NULL THEN '1' ELSE '0' END AS `pin__order_min_amount`, CASE WHEN t.`outstanding_limit` IS NULL + THEN '1' ELSE '0' END AS `pin__outstanding_limit`, CASE WHEN t.`parent` IS NULL THEN '1' ELSE + '0' END AS `pin__parent`, CASE WHEN t.`price_level` IS NULL THEN '1' ELSE '0' END AS `pin__price_level`, + CASE WHEN t.`remise_client` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_client`, + CASE WHEN t.`remise_supplier` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_supplier`, + CASE WHEN t.`status` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__status`, CASE + WHEN t.`statut` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__statut`, CASE WHEN + t.`supplier_order_min_amount` IS NULL THEN '1' ELSE '0' END AS `pin__supplier_order_min_amount`, + CASE WHEN t.`supplier_rate` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__supplier_rate`, + CASE WHEN t.`transport_mode` IS NULL THEN '1' ELSE '0' END AS `pin__transport_mode`, CASE WHEN + t.`transport_mode_supplier` IS NULL THEN '1' ELSE '0' END AS `pin__transport_mode_supplier`, CASE + WHEN t.`tva_assuj` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tva_assuj`, CASE + WHEN t.`vat_reverse_charge` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__vat_reverse_charge` + FROM `llx_societe` t + raw_llx_actioncomm: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` + AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, + CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` + AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` + AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS + `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, + CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, + CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` + AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) + AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, + CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` + AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` + AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` + AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS + CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) + AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, + CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` + AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS + `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` + AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` + AS CHAR) AS `extraparams` FROM `llx_actioncomm` t + raw_llx_actioncomm_resources: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` + AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` + AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` + AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + address: + param: address + p__country: + param: country + email: + param: email + nom: + param: name + phone: + param: phone + kind: phone + town: + param: town + zip: + param: zip + pin__capital: + literal: '1' + pin__client: + literal: '1' + pin__cond_reglement: + literal: '1' + pin__cond_reglement_supplier: + literal: '1' + pin__customer_bad: + literal: '1' + pin__customer_rate: + literal: '1' + pin__entity: + literal: '1' + pin__fk_account: + literal: '1' + pin__fk_barcode_type: + literal: '1' + pin__fk_departement: + literal: '1' + pin__fk_effectif: + literal: '1' + pin__fk_forme_juridique: + literal: '1' + pin__fk_incoterms: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_shipping_method: + literal: '1' + pin__fk_stcomm: + literal: '1' + pin__fk_typent: + literal: '1' + fk_user_creat: + param: bot_user_id + fk_user_modif: + param: bot_user_id + pin__fk_warehouse: + literal: '1' + pin__fournisseur: + literal: '1' + pin__geolat: + literal: '1' + pin__geolong: + literal: '1' + pin__geopoint: + literal: '1' + pin__localtax1_assuj: + literal: '1' + pin__localtax1_value: + literal: '1' + pin__localtax2_assuj: + literal: '1' + pin__localtax2_value: + literal: '1' + pin__mode_reglement: + literal: '1' + pin__mode_reglement_supplier: + literal: '1' + pin__order_min_amount: + literal: '1' + pin__outstanding_limit: + literal: '1' + pin__parent: + literal: '1' + pin__price_level: + literal: '1' + pin__remise_client: + literal: '1' + pin__remise_supplier: + literal: '1' + socialnetworks: + literal: 'null' + pin__status: + literal: '1' + pin__statut: + literal: '1' + pin__supplier_order_min_amount: + literal: '1' + pin__supplier_rate: + literal: '1' + pin__transport_mode: + literal: '1' + pin__transport_mode_supplier: + literal: '1' + pin__tva_assuj: + literal: '1' + pin__vat_reverse_charge: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm + match: + code: + literal: AC_COMPANY_CREATE + entity: + literal: '1' + event_paid: + literal: '0' + fk_action: + literal: '40' + fk_parent: + literal: '0' + fk_user_action: + param: bot_user_id + fk_user_author: + param: bot_user_id + fulldayevent: + literal: '0' + percent: + literal: '-1' + priority: + literal: '0' + status: + literal: '0' + transparency: + literal: '0' + visibility: + literal: default + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources + match: + element_type: + literal: user + fk_element: + param: bot_user_id + mandatory: + literal: '0' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: + llx_actioncomm: raw_llx_actioncomm + llx_actioncomm_resources: raw_llx_actioncomm_resources + settle_s: 1 + settle_attempts: 5 + new_row_fields: + llx_actioncomm: + - datec + - datep + - datep2 + - durationp + - elementtype + - email_from + - email_msgid + - email_sender + - email_subject + - email_to + - email_tobcc + - email_tocc + - errors_to + - extraparams + - fk_soc + - id + - import_key + - ip + - label + - location + - note + - recurid + - recurrule + - ref + - ref_ext + - reply_to + - tms + llx_actioncomm_resources: + - answer_status + - fk_actioncomm + - rowid + - transparency + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml new file mode 100644 index 0000000000..c54cbd55ef --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml @@ -0,0 +1,106 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_customer, found, fields address, country, email, name, phone, town, zip. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key`, (SELECT CAST(a1.`label` + AS CHAR) FROM `llx_c_country` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_pays` AS BINARY)) + AS `p__country` FROM `llx_societe` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + address: + param: address + p__country: + param: country + email: + param: email + nom: + param: name + phone: + param: phone + kind: phone + town: + param: town + zip: + param: zip + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml new file mode 100644 index 0000000000..46aa29ec26 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml @@ -0,0 +1,496 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, create, fields customer, invoice_date, town, linesx2. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` + AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, + CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` + AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, + CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` + AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) + AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` + AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) + AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS + `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, + CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, + CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, + CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, + CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, + CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` + AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` + AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, + CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS + `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS + `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) + AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` + AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` + AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, + CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS + `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, + CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, + CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, + CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) + AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, + CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, CASE WHEN t.`datef` + = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, (SELECT CAST(a1.`town` AS CHAR) + FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town`, CASE WHEN t.`close_missing_amount` + IS NULL THEN '1' ELSE '0' END AS `pin__close_missing_amount`, CASE WHEN t.`date_closing` IS NULL + THEN '1' ELSE '0' END AS `pin__date_closing`, CASE WHEN t.`date_pointoftax` IS NULL THEN '1' ELSE + '0' END AS `pin__date_pointoftax`, CASE WHEN t.`date_valid` IS NULL THEN '1' ELSE '0' END AS `pin__date_valid`, + CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE + WHEN t.`fk_account` IS NULL THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_cond_reglement` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_cond_reglement`, CASE WHEN t.`fk_fac_rec_source` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_fac_rec_source`, CASE WHEN t.`fk_facture_source` IS + NULL THEN '1' ELSE '0' END AS `pin__fk_facture_source`, CASE WHEN t.`fk_incoterms` = (SELECT s.`fk_incoterms` + FROM `llx_societe` s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_incoterms`, + CASE WHEN t.`fk_mode_reglement` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_mode_reglement`, + CASE WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_societe` s WHERE s.`rowid` + = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_projet` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_projet`, CASE WHEN t.`fk_statut` = (SELECT s.`statut` FROM `llx_societe` + s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_statut`, CASE WHEN t.`fk_transport_mode` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_transport_mode`, CASE WHEN t.`fk_user_closing` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_user_closing`, CASE WHEN t.`fk_user_modif` IS NULL THEN '1' + ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_user_valid` IS NULL THEN '1' ELSE '0' END + AS `pin__fk_user_valid`, CASE WHEN t.`is_also_delivery_note` = CAST('0' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__is_also_delivery_note`, CASE WHEN t.`localtax1` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax1`, CASE WHEN t.`localtax2` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax2`, CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, CASE WHEN t.`paye` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__paye`, CASE WHEN t.`prorata_discount` IS NULL THEN '1' ELSE '0' + END AS `pin__prorata_discount`, CASE WHEN t.`remise` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE + '0' END AS `pin__remise`, CASE WHEN t.`remise_absolue` = CAST('0' AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `pin__remise_absolue`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`retained_warranty` = CAST('0' AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty`, CASE WHEN t.`retained_warranty_date_limit` + IS NULL THEN '1' ELSE '0' END AS `pin__retained_warranty_date_limit`, CASE WHEN t.`retained_warranty_fk_cond_reglement` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty_fk_cond_reglement`, + CASE WHEN t.`revenuestamp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__revenuestamp`, + CASE WHEN t.`situation_counter` IS NULL THEN '1' ELSE '0' END AS `pin__situation_counter`, CASE + WHEN t.`situation_cycle_ref` IS NULL THEN '1' ELSE '0' END AS `pin__situation_cycle_ref`, CASE + WHEN t.`situation_final` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_final`, + CASE WHEN t.`subtype` IS NULL THEN '1' ELSE '0' END AS `pin__subtype`, CASE WHEN t.`type` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__type` FROM `llx_facture` t + sql_query_params: + v0: &id002 + param: invoice_date + rows_llx_facturedet: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_facture` + AS CHAR) AS `fk_facture`, CAST(t.`fk_parent_line` AS CHAR) AS `fk_parent_line`, CAST(t.`fk_product` + AS CHAR) AS `fk_product`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS + `description`, CAST(t.`vat_src_code` AS CHAR) AS `vat_src_code`, CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, + CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, + CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, + CAST(t.`qty` AS CHAR) AS `qty`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise` + AS CHAR) AS `remise`, CAST(t.`fk_remise_except` AS CHAR) AS `fk_remise_except`, CAST(t.`subprice` + AS CHAR) AS `subprice`, CAST(t.`subprice_ttc` AS CHAR) AS `subprice_ttc`, CAST(t.`price` AS CHAR) + AS `price`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, + CAST(t.`total_localtax1` AS CHAR) AS `total_localtax1`, CAST(t.`total_localtax2` AS CHAR) AS `total_localtax2`, + CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, CAST(t.`product_type` AS CHAR) AS `product_type`, + CAST(t.`date_start` AS CHAR) AS `date_start`, CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`info_bits` + AS CHAR) AS `info_bits`, CAST(t.`buy_price_ht` AS CHAR) AS `buy_price_ht`, CAST(t.`fk_product_fournisseur_price` + AS CHAR) AS `fk_product_fournisseur_price`, CAST(t.`batch` AS CHAR) AS `batch`, CAST(t.`fk_warehouse` + AS CHAR) AS `fk_warehouse`, CAST(t.`special_code` AS CHAR) AS `special_code`, CAST(t.`rang` AS + CHAR) AS `rang`, CAST(t.`fk_contract_line` AS CHAR) AS `fk_contract_line`, CAST(t.`fk_unit` AS + CHAR) AS `fk_unit`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_code_ventilation` + AS CHAR) AS `fk_code_ventilation`, CAST(t.`situation_percent` AS CHAR) AS `situation_percent`, + CAST(t.`fk_prev_id` AS CHAR) AS `fk_prev_id`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, + CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_subprice` + AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, + CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` + AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CAST(a2.`nom` AS CHAR) FROM `llx_societe` + a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS + `main__customer`, CASE WHEN (SELECT a1.`datef` FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) + = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `main__invoice_date`, (SELECT (SELECT CAST(a2.`town` + AS CHAR) FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` + = t.`fk_facture`) AS `main__town`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE + CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_0_product_ref`, CASE + WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v1)s AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `p__lines_0_qty`, CASE WHEN t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE WHEN t.`date_end` IS NULL THEN '1' ELSE '0' + END AS `pin__date_end`, CASE WHEN t.`date_start` IS NULL THEN '1' ELSE '0' END AS `pin__date_start`, + CASE WHEN t.`fk_code_ventilation` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, + CASE WHEN t.`fk_contract_line` IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE + WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` + = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_prev_id`, CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, CASE WHEN t.`fk_remise_except` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE + '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` FROM `llx_facture` + s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, CASE WHEN + t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_user_modif`, + CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_warehouse`, + CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__info_bits`, + CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_tx`, + CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_tx`, + CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` FROM `llx_facture` s WHERE s.`rowid` + = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, CASE WHEN t.`multicurrency_subprice` + = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END + AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE WHEN t.`price` IS NULL THEN '1' + ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT s.`type` FROM `llx_facture` + s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, CASE WHEN t.`rang` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN t.`remise` = (SELECT + s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__remise`, + CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_percent`, + CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_percent`, + CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__special_code`, + CASE WHEN t.`subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__subprice`, CASE WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__subprice_ttc`, CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax1`, + CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) + THEN '1' ELSE '0' END AS `pin__total_localtax2`, CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM + `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx`, (SELECT + CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` + AS BINARY)) AS `p__lines_1_product_ref`, CASE WHEN CAST(%(v2)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_1_qty`, CASE WHEN + t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE + WHEN t.`date_end` IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS + NULL THEN '1' ELSE '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_multicurrency` = (SELECT + s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' + END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE '0' END AS + `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, + CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, + CASE WHEN t.`fk_remise_except` IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE + WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` + = (SELECT s.`fk_user_author` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE + '0' END AS `pin__fk_user_author`, CASE WHEN t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__info_bits`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, + CASE WHEN t.`multicurrency_subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` + = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE + WHEN t.`price` IS NULL THEN '1' ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT + s.`type` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, + CASE WHEN t.`rang` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN + t.`remise` = (SELECT s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' + ELSE '0' END AS `pin__remise`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__situation_percent`, CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__special_code`, CASE WHEN t.`subprice` = (SELECT s.`price` FROM + `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__subprice`, CASE + WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__subprice_ttc`, + CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) + THEN '1' ELSE '0' END AS `pin__total_localtax1`, CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax2`, + CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__tva_tx` FROM `llx_facturedet` t + sql_query_params: + v0: *id002 + v1: + param: lines.0.qty + v2: + param: lines.1.qty +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__customer: + param: customer + p__invoice_date: + literal: '1' + p__town: + param: town + pin__close_missing_amount: + literal: '1' + pin__date_closing: + literal: '1' + pin__date_pointoftax: + literal: '1' + pin__date_valid: + literal: '1' + pin__entity: + literal: '1' + pin__fk_account: + literal: '1' + pin__fk_cond_reglement: + literal: '1' + pin__fk_fac_rec_source: + literal: '1' + pin__fk_facture_source: + literal: '1' + pin__fk_incoterms: + literal: '1' + pin__fk_mode_reglement: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_projet: + literal: '1' + pin__fk_statut: + literal: '1' + pin__fk_transport_mode: + literal: '1' + fk_user_author: + param: bot_user_id + pin__fk_user_closing: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_user_valid: + literal: '1' + pin__is_also_delivery_note: + literal: '1' + pin__localtax1: + literal: '1' + pin__localtax2: + literal: '1' + multicurrency_code: + literal: EUR + pin__multicurrency_tx: + literal: '1' + pin__paye: + literal: '1' + pin__prorata_discount: + literal: '1' + pin__remise: + literal: '1' + pin__remise_absolue: + literal: '1' + pin__remise_percent: + literal: '1' + pin__retained_warranty: + literal: '1' + pin__retained_warranty_date_limit: + literal: '1' + pin__retained_warranty_fk_cond_reglement: + literal: '1' + pin__revenuestamp: + literal: '1' + pin__situation_counter: + literal: '1' + pin__situation_cycle_ref: + literal: '1' + pin__situation_final: + literal: '1' + pin__subtype: + literal: '1' + pin__type: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__invoice_date: + literal: '1' + main__town: + param: town + p__lines_0_product_ref: + param: lines.0.product_ref + p__lines_0_qty: + literal: '1' + pin__buy_price_ht: + literal: '1' + pin__date_end: + literal: '1' + pin__date_start: + literal: '1' + pin__fk_code_ventilation: + literal: '1' + pin__fk_contract_line: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_parent_line: + literal: '1' + pin__fk_prev_id: + literal: '1' + pin__fk_product_fournisseur_price: + literal: '1' + pin__fk_remise_except: + literal: '1' + pin__fk_unit: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_warehouse: + literal: '1' + pin__info_bits: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__multicurrency_code: + literal: '1' + pin__multicurrency_subprice: + literal: '1' + pin__multicurrency_subprice_ttc: + literal: '1' + pin__price: + literal: '1' + pin__product_type: + literal: '1' + pin__rang: + literal: '1' + pin__remise: + literal: '1' + pin__remise_percent: + literal: '1' + pin__situation_percent: + literal: '1' + pin__special_code: + literal: '1' + pin__subprice: + literal: '1' + pin__subprice_ttc: + literal: '1' + pin__total_localtax1: + literal: '1' + pin__total_localtax2: + literal: '1' + pin__tva_tx: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__invoice_date: + literal: '1' + main__town: + param: town + p__lines_1_product_ref: + param: lines.1.product_ref + p__lines_1_qty: + literal: '1' + pin__buy_price_ht: + literal: '1' + pin__date_end: + literal: '1' + pin__date_start: + literal: '1' + pin__fk_code_ventilation: + literal: '1' + pin__fk_contract_line: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_parent_line: + literal: '1' + pin__fk_prev_id: + literal: '1' + pin__fk_product_fournisseur_price: + literal: '1' + pin__fk_remise_except: + literal: '1' + pin__fk_unit: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_warehouse: + literal: '1' + pin__info_bits: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__multicurrency_code: + literal: '1' + pin__multicurrency_subprice: + literal: '1' + pin__multicurrency_subprice_ttc: + literal: '1' + pin__price: + literal: '1' + pin__product_type: + literal: '1' + pin__rang: + literal: '1' + pin__remise: + literal: '1' + pin__remise_percent: + literal: '1' + pin__situation_percent: + literal: '1' + pin__special_code: + literal: '1' + pin__subprice: + literal: '1' + pin__subprice_ttc: + literal: '1' + pin__total_localtax1: + literal: '1' + pin__total_localtax2: + literal: '1' + pin__tva_tx: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_facture + mode: skip + reason: read in full, every column, by this contract's own surface + - table: llx_facturedet + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml new file mode 100644 index 0000000000..e9e6d0f41e --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml @@ -0,0 +1,93 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, found, fields customer, invoice_date, town, linesx2. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` + AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, + CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` + AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, + CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` + AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) + AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` + AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) + AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS + `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, + CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, + CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, + CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, + CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, + CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` + AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` + AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, + CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS + `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS + `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) + AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` + AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` + AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, + CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS + `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, + CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, + CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, + CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) + AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, + CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, CASE WHEN t.`datef` + = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, (SELECT CAST(a1.`town` AS CHAR) + FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town` FROM `llx_facture` t + sql_query_params: + v0: + param: invoice_date +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__customer: + param: customer + p__invoice_date: + literal: '1' + p__town: + param: town + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_facture + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml new file mode 100644 index 0000000000..18d772fe81 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml @@ -0,0 +1,484 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, create, fields customer, town, linesx2. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` + AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, + CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` + AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, + CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` + AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) + AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` + AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) + AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS + `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, + CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, + CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, + CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, + CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, + CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` + AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` + AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, + CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS + `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS + `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) + AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` + AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` + AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, + CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS + `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, + CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, + CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, + CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) + AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, + CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, (SELECT CAST(a1.`town` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town`, CASE WHEN t.`close_missing_amount` + IS NULL THEN '1' ELSE '0' END AS `pin__close_missing_amount`, CASE WHEN t.`date_closing` IS NULL + THEN '1' ELSE '0' END AS `pin__date_closing`, CASE WHEN t.`date_pointoftax` IS NULL THEN '1' ELSE + '0' END AS `pin__date_pointoftax`, CASE WHEN t.`date_valid` IS NULL THEN '1' ELSE '0' END AS `pin__date_valid`, + CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE + WHEN t.`fk_account` IS NULL THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_cond_reglement` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_cond_reglement`, CASE WHEN t.`fk_fac_rec_source` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_fac_rec_source`, CASE WHEN t.`fk_facture_source` IS + NULL THEN '1' ELSE '0' END AS `pin__fk_facture_source`, CASE WHEN t.`fk_incoterms` = (SELECT s.`fk_incoterms` + FROM `llx_societe` s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_incoterms`, + CASE WHEN t.`fk_mode_reglement` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_mode_reglement`, + CASE WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_societe` s WHERE s.`rowid` + = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_projet` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_projet`, CASE WHEN t.`fk_statut` = (SELECT s.`statut` FROM `llx_societe` + s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_statut`, CASE WHEN t.`fk_transport_mode` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_transport_mode`, CASE WHEN t.`fk_user_closing` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_user_closing`, CASE WHEN t.`fk_user_modif` IS NULL THEN '1' + ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_user_valid` IS NULL THEN '1' ELSE '0' END + AS `pin__fk_user_valid`, CASE WHEN t.`is_also_delivery_note` = CAST('0' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__is_also_delivery_note`, CASE WHEN t.`localtax1` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax1`, CASE WHEN t.`localtax2` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax2`, CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, CASE WHEN t.`paye` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__paye`, CASE WHEN t.`prorata_discount` IS NULL THEN '1' ELSE '0' + END AS `pin__prorata_discount`, CASE WHEN t.`remise` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE + '0' END AS `pin__remise`, CASE WHEN t.`remise_absolue` = CAST('0' AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `pin__remise_absolue`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`retained_warranty` = CAST('0' AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty`, CASE WHEN t.`retained_warranty_date_limit` + IS NULL THEN '1' ELSE '0' END AS `pin__retained_warranty_date_limit`, CASE WHEN t.`retained_warranty_fk_cond_reglement` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty_fk_cond_reglement`, + CASE WHEN t.`revenuestamp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__revenuestamp`, + CASE WHEN t.`situation_counter` IS NULL THEN '1' ELSE '0' END AS `pin__situation_counter`, CASE + WHEN t.`situation_cycle_ref` IS NULL THEN '1' ELSE '0' END AS `pin__situation_cycle_ref`, CASE + WHEN t.`situation_final` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_final`, + CASE WHEN t.`subtype` IS NULL THEN '1' ELSE '0' END AS `pin__subtype`, CASE WHEN t.`type` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__type` FROM `llx_facture` t + rows_llx_facturedet: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_facture` + AS CHAR) AS `fk_facture`, CAST(t.`fk_parent_line` AS CHAR) AS `fk_parent_line`, CAST(t.`fk_product` + AS CHAR) AS `fk_product`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS + `description`, CAST(t.`vat_src_code` AS CHAR) AS `vat_src_code`, CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, + CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, + CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, + CAST(t.`qty` AS CHAR) AS `qty`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise` + AS CHAR) AS `remise`, CAST(t.`fk_remise_except` AS CHAR) AS `fk_remise_except`, CAST(t.`subprice` + AS CHAR) AS `subprice`, CAST(t.`subprice_ttc` AS CHAR) AS `subprice_ttc`, CAST(t.`price` AS CHAR) + AS `price`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, + CAST(t.`total_localtax1` AS CHAR) AS `total_localtax1`, CAST(t.`total_localtax2` AS CHAR) AS `total_localtax2`, + CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, CAST(t.`product_type` AS CHAR) AS `product_type`, + CAST(t.`date_start` AS CHAR) AS `date_start`, CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`info_bits` + AS CHAR) AS `info_bits`, CAST(t.`buy_price_ht` AS CHAR) AS `buy_price_ht`, CAST(t.`fk_product_fournisseur_price` + AS CHAR) AS `fk_product_fournisseur_price`, CAST(t.`batch` AS CHAR) AS `batch`, CAST(t.`fk_warehouse` + AS CHAR) AS `fk_warehouse`, CAST(t.`special_code` AS CHAR) AS `special_code`, CAST(t.`rang` AS + CHAR) AS `rang`, CAST(t.`fk_contract_line` AS CHAR) AS `fk_contract_line`, CAST(t.`fk_unit` AS + CHAR) AS `fk_unit`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_code_ventilation` + AS CHAR) AS `fk_code_ventilation`, CAST(t.`situation_percent` AS CHAR) AS `situation_percent`, + CAST(t.`fk_prev_id` AS CHAR) AS `fk_prev_id`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, + CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_subprice` + AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, + CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` + AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CAST(a2.`nom` AS CHAR) FROM `llx_societe` + a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS + `main__customer`, (SELECT (SELECT CAST(a2.`town` AS CHAR) FROM `llx_societe` a2 WHERE a2.`rowid` + = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS `main__town`, (SELECT + CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` + AS BINARY)) AS `p__lines_0_product_ref`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND t.`qty` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_0_qty`, CASE WHEN + t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE + WHEN t.`date_end` IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS + NULL THEN '1' ELSE '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_multicurrency` = (SELECT + s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' + END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE '0' END AS + `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, + CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, + CASE WHEN t.`fk_remise_except` IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE + WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` + = (SELECT s.`fk_user_author` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE + '0' END AS `pin__fk_user_author`, CASE WHEN t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__info_bits`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, + CASE WHEN t.`multicurrency_subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` + = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE + WHEN t.`price` IS NULL THEN '1' ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT + s.`type` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, + CASE WHEN t.`rang` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN + t.`remise` = (SELECT s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' + ELSE '0' END AS `pin__remise`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__situation_percent`, CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__special_code`, CASE WHEN t.`subprice` = (SELECT s.`price` FROM + `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__subprice`, CASE + WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__subprice_ttc`, + CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) + THEN '1' ELSE '0' END AS `pin__total_localtax1`, CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax2`, + CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__tva_tx`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE + CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, CASE + WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v1)s AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `p__lines_1_qty`, CASE WHEN t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE WHEN t.`date_end` IS NULL THEN '1' ELSE '0' + END AS `pin__date_end`, CASE WHEN t.`date_start` IS NULL THEN '1' ELSE '0' END AS `pin__date_start`, + CASE WHEN t.`fk_code_ventilation` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, + CASE WHEN t.`fk_contract_line` IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE + WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` + = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_prev_id`, CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, CASE WHEN t.`fk_remise_except` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE + '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` FROM `llx_facture` + s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, CASE WHEN + t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_user_modif`, + CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_warehouse`, + CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__info_bits`, + CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_tx`, + CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_tx`, + CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` FROM `llx_facture` s WHERE s.`rowid` + = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, CASE WHEN t.`multicurrency_subprice` + = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END + AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE WHEN t.`price` IS NULL THEN '1' + ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT s.`type` FROM `llx_facture` + s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, CASE WHEN t.`rang` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN t.`remise` = (SELECT + s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__remise`, + CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_percent`, + CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_percent`, + CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__special_code`, + CASE WHEN t.`subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__subprice`, CASE WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__subprice_ttc`, CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax1`, + CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) + THEN '1' ELSE '0' END AS `pin__total_localtax2`, CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM + `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx` FROM + `llx_facturedet` t + sql_query_params: + v0: + param: lines.0.qty + v1: + param: lines.1.qty +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__customer: + param: customer + p__town: + param: town + pin__close_missing_amount: + literal: '1' + pin__date_closing: + literal: '1' + pin__date_pointoftax: + literal: '1' + pin__date_valid: + literal: '1' + pin__entity: + literal: '1' + pin__fk_account: + literal: '1' + pin__fk_cond_reglement: + literal: '1' + pin__fk_fac_rec_source: + literal: '1' + pin__fk_facture_source: + literal: '1' + pin__fk_incoterms: + literal: '1' + pin__fk_mode_reglement: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_projet: + literal: '1' + pin__fk_statut: + literal: '1' + pin__fk_transport_mode: + literal: '1' + fk_user_author: + param: bot_user_id + pin__fk_user_closing: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_user_valid: + literal: '1' + pin__is_also_delivery_note: + literal: '1' + pin__localtax1: + literal: '1' + pin__localtax2: + literal: '1' + multicurrency_code: + literal: EUR + pin__multicurrency_tx: + literal: '1' + pin__paye: + literal: '1' + pin__prorata_discount: + literal: '1' + pin__remise: + literal: '1' + pin__remise_absolue: + literal: '1' + pin__remise_percent: + literal: '1' + pin__retained_warranty: + literal: '1' + pin__retained_warranty_date_limit: + literal: '1' + pin__retained_warranty_fk_cond_reglement: + literal: '1' + pin__revenuestamp: + literal: '1' + pin__situation_counter: + literal: '1' + pin__situation_cycle_ref: + literal: '1' + pin__situation_final: + literal: '1' + pin__subtype: + literal: '1' + pin__type: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__town: + param: town + p__lines_0_product_ref: + param: lines.0.product_ref + p__lines_0_qty: + literal: '1' + pin__buy_price_ht: + literal: '1' + pin__date_end: + literal: '1' + pin__date_start: + literal: '1' + pin__fk_code_ventilation: + literal: '1' + pin__fk_contract_line: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_parent_line: + literal: '1' + pin__fk_prev_id: + literal: '1' + pin__fk_product_fournisseur_price: + literal: '1' + pin__fk_remise_except: + literal: '1' + pin__fk_unit: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_warehouse: + literal: '1' + pin__info_bits: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__multicurrency_code: + literal: '1' + pin__multicurrency_subprice: + literal: '1' + pin__multicurrency_subprice_ttc: + literal: '1' + pin__price: + literal: '1' + pin__product_type: + literal: '1' + pin__rang: + literal: '1' + pin__remise: + literal: '1' + pin__remise_percent: + literal: '1' + pin__situation_percent: + literal: '1' + pin__special_code: + literal: '1' + pin__subprice: + literal: '1' + pin__subprice_ttc: + literal: '1' + pin__total_localtax1: + literal: '1' + pin__total_localtax2: + literal: '1' + pin__tva_tx: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__town: + param: town + p__lines_1_product_ref: + param: lines.1.product_ref + p__lines_1_qty: + literal: '1' + pin__buy_price_ht: + literal: '1' + pin__date_end: + literal: '1' + pin__date_start: + literal: '1' + pin__fk_code_ventilation: + literal: '1' + pin__fk_contract_line: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_parent_line: + literal: '1' + pin__fk_prev_id: + literal: '1' + pin__fk_product_fournisseur_price: + literal: '1' + pin__fk_remise_except: + literal: '1' + pin__fk_unit: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_warehouse: + literal: '1' + pin__info_bits: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__multicurrency_code: + literal: '1' + pin__multicurrency_subprice: + literal: '1' + pin__multicurrency_subprice_ttc: + literal: '1' + pin__price: + literal: '1' + pin__product_type: + literal: '1' + pin__rang: + literal: '1' + pin__remise: + literal: '1' + pin__remise_percent: + literal: '1' + pin__situation_percent: + literal: '1' + pin__special_code: + literal: '1' + pin__subprice: + literal: '1' + pin__subprice_ttc: + literal: '1' + pin__total_localtax1: + literal: '1' + pin__total_localtax2: + literal: '1' + pin__tva_tx: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_facture + mode: skip + reason: read in full, every column, by this contract's own surface + - table: llx_facturedet + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml new file mode 100644 index 0000000000..108cdf3e93 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml @@ -0,0 +1,88 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, found, fields customer, town, linesx2. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` + AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, + CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` + AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, + CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` + AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) + AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` + AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) + AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS + `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, + CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, + CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, + CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, + CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, + CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` + AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` + AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, + CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS + `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS + `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) + AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` + AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` + AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, + CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS + `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, + CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, + CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, + CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) + AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, + CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, (SELECT CAST(a1.`town` + AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town` FROM `llx_facture` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__customer: + param: customer + p__town: + param: town + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_facture + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml new file mode 100644 index 0000000000..74239e91f1 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml @@ -0,0 +1,426 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_product, create, fields label, price_excl_vat, ref, vat_rate. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_parent` AS CHAR) + AS `fk_parent`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS `description`, + CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`note` AS CHAR) AS `note`, CAST(t.`customcode` + AS CHAR) AS `customcode`, CAST(t.`fk_country` AS CHAR) AS `fk_country`, CAST(t.`fk_state` AS CHAR) + AS `fk_state`, CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, + CAST(t.`price_min` AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, + CAST(t.`price_base_type` AS CHAR) AS `price_base_type`, CAST(t.`price_label` AS CHAR) AS `price_label`, + CAST(t.`cost_price` AS CHAR) AS `cost_price`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, + CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, + CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, + CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, + CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, + CAST(t.`tosell` AS CHAR) AS `tosell`, CAST(t.`tobuy` AS CHAR) AS `tobuy`, CAST(t.`tobatch` AS + CHAR) AS `tobatch`, CAST(t.`sell_or_eat_by_mandatory` AS CHAR) AS `sell_or_eat_by_mandatory`, + CAST(t.`batch_mask` AS CHAR) AS `batch_mask`, CAST(t.`fk_product_type` AS CHAR) AS `fk_product_type`, + CAST(t.`duration` AS CHAR) AS `duration`, CAST(t.`seuil_stock_alerte` AS CHAR) AS `seuil_stock_alerte`, + CAST(t.`url` AS CHAR) AS `url`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`accountancy_code_sell` AS CHAR) AS `accountancy_code_sell`, + CAST(t.`accountancy_code_sell_intra` AS CHAR) AS `accountancy_code_sell_intra`, CAST(t.`accountancy_code_sell_export` + AS CHAR) AS `accountancy_code_sell_export`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`accountancy_code_buy_intra` AS CHAR) AS `accountancy_code_buy_intra`, CAST(t.`accountancy_code_buy_export` + AS CHAR) AS `accountancy_code_buy_export`, CAST(t.`partnumber` AS CHAR) AS `partnumber`, CAST(t.`net_measure` + AS CHAR) AS `net_measure`, CAST(t.`net_measure_units` AS CHAR) AS `net_measure_units`, CAST(t.`weight` + AS CHAR) AS `weight`, CAST(t.`weight_units` AS CHAR) AS `weight_units`, CAST(t.`length` AS CHAR) + AS `length`, CAST(t.`length_units` AS CHAR) AS `length_units`, CAST(t.`width` AS CHAR) AS `width`, + CAST(t.`width_units` AS CHAR) AS `width_units`, CAST(t.`height` AS CHAR) AS `height`, CAST(t.`height_units` + AS CHAR) AS `height_units`, CAST(t.`surface` AS CHAR) AS `surface`, CAST(t.`surface_units` AS + CHAR) AS `surface_units`, CAST(t.`volume` AS CHAR) AS `volume`, CAST(t.`volume_units` AS CHAR) + AS `volume_units`, CAST(t.`stockable_product` AS CHAR) AS `stockable_product`, CAST(t.`stock` + AS CHAR) AS `stock`, CAST(t.`pmp` AS CHAR) AS `pmp`, CAST(t.`fifo` AS CHAR) AS `fifo`, CAST(t.`lifo` + AS CHAR) AS `lifo`, CAST(t.`fk_default_warehouse` AS CHAR) AS `fk_default_warehouse`, CAST(t.`fk_default_bom` + AS CHAR) AS `fk_default_bom`, CAST(t.`fk_default_workstation` AS CHAR) AS `fk_default_workstation`, + CAST(t.`canvas` AS CHAR) AS `canvas`, CAST(t.`finished` AS CHAR) AS `finished`, CAST(t.`lifetime` + AS CHAR) AS `lifetime`, CAST(t.`qc_frequency` AS CHAR) AS `qc_frequency`, CAST(t.`hidden` AS CHAR) + AS `hidden`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, + CAST(t.`fk_price_expression` AS CHAR) AS `fk_price_expression`, CAST(t.`desiredstock` AS CHAR) + AS `desiredstock`, CAST(t.`fk_unit` AS CHAR) AS `fk_unit`, CAST(t.`price_autogen` AS CHAR) AS + `price_autogen`, CAST(t.`fk_project` AS CHAR) AS `fk_project`, CAST(t.`mandatory_period` AS CHAR) + AS `mandatory_period`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CASE WHEN CAST(%(v0)s + AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `p__price_excl_vat`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate`, CASE WHEN + t.`cost_price` IS NULL THEN '1' ELSE '0' END AS `pin__cost_price`, CASE WHEN t.`desiredstock` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__desiredstock`, CASE WHEN t.`entity` + = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fifo` IS NULL + THEN '1' ELSE '0' END AS `pin__fifo`, CASE WHEN t.`finished` IS NULL THEN '1' ELSE '0' END AS + `pin__finished`, CASE WHEN t.`fk_barcode_type` IS NULL THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, + CASE WHEN t.`fk_country` IS NULL THEN '1' ELSE '0' END AS `pin__fk_country`, CASE WHEN t.`fk_default_bom` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_default_bom`, CASE WHEN t.`fk_default_warehouse` IS + NULL THEN '1' ELSE '0' END AS `pin__fk_default_warehouse`, CASE WHEN t.`fk_default_workstation` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_default_workstation`, CASE WHEN t.`fk_parent` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_parent`, CASE WHEN t.`fk_price_expression` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_price_expression`, CASE WHEN t.`fk_product_type` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_product_type`, CASE WHEN t.`fk_project` IS + NULL THEN '1' ELSE '0' END AS `pin__fk_project`, CASE WHEN t.`fk_state` IS NULL THEN '1' ELSE + '0' END AS `pin__fk_state`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, + CASE WHEN t.`height` IS NULL THEN '1' ELSE '0' END AS `pin__height`, CASE WHEN t.`height_units` + IS NULL THEN '1' ELSE '0' END AS `pin__height_units`, CASE WHEN t.`hidden` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__hidden`, CASE WHEN t.`length` IS NULL THEN '1' ELSE '0' END AS + `pin__length`, CASE WHEN t.`length_units` IS NULL THEN '1' ELSE '0' END AS `pin__length_units`, + CASE WHEN t.`lifetime` IS NULL THEN '1' ELSE '0' END AS `pin__lifetime`, CASE WHEN t.`lifo` IS + NULL THEN '1' ELSE '0' END AS `pin__lifo`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`mandatory_period` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__mandatory_period`, CASE WHEN t.`net_measure` IS NULL THEN '1' ELSE + '0' END AS `pin__net_measure`, CASE WHEN t.`net_measure_units` IS NULL THEN '1' ELSE '0' END AS + `pin__net_measure_units`, CASE WHEN t.`pmp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__pmp`, CASE WHEN t.`price_autogen` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__price_autogen`, CASE WHEN t.`price_min` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' + END AS `pin__price_min`, CASE WHEN t.`price_min_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE + '0' END AS `pin__price_min_ttc`, CASE WHEN t.`qc_frequency` IS NULL THEN '1' ELSE '0' END AS `pin__qc_frequency`, + CASE WHEN t.`recuperableonly` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__recuperableonly`, + CASE WHEN t.`sell_or_eat_by_mandatory` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `pin__sell_or_eat_by_mandatory`, CASE WHEN t.`seuil_stock_alerte` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__seuil_stock_alerte`, CASE WHEN t.`stock` IS NULL THEN '1' ELSE + '0' END AS `pin__stock`, CASE WHEN t.`stockable_product` = CAST('1' AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `pin__stockable_product`, CASE WHEN t.`surface` IS NULL THEN '1' ELSE '0' END + AS `pin__surface`, CASE WHEN t.`surface_units` IS NULL THEN '1' ELSE '0' END AS `pin__surface_units`, + CASE WHEN t.`tobatch` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tobatch`, CASE + WHEN t.`tobuy` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tobuy`, CASE WHEN + t.`tosell` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tosell`, CASE WHEN t.`volume` + IS NULL THEN '1' ELSE '0' END AS `pin__volume`, CASE WHEN t.`volume_units` IS NULL THEN '1' ELSE + '0' END AS `pin__volume_units`, CASE WHEN t.`weight` IS NULL THEN '1' ELSE '0' END AS `pin__weight`, + CASE WHEN t.`weight_units` IS NULL THEN '1' ELSE '0' END AS `pin__weight_units`, CASE WHEN t.`width` + IS NULL THEN '1' ELSE '0' END AS `pin__width`, CASE WHEN t.`width_units` IS NULL THEN '1' ELSE + '0' END AS `pin__width_units` FROM `llx_product` t + sql_query_params: + v0: + param: price_excl_vat + v1: + param: vat_rate + raw_llx_actioncomm: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` + AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, + CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` + AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` + AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS + `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, + CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, + CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` + AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) + AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, + CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` + AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` + AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` + AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS + CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) + AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, + CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` + AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS + `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` + AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` + AS CHAR) AS `extraparams` FROM `llx_actioncomm` t + raw_llx_actioncomm_resources: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` + AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` + AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` + AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` + t + raw_llx_product_price: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`entity` + AS CHAR) AS `entity`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_product` AS CHAR) AS `fk_product`, + CAST(t.`date_price` AS CHAR) AS `date_price`, CAST(t.`price_level` AS CHAR) AS `price_level`, + CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, CAST(t.`price_min` + AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, CAST(t.`price_base_type` + AS CHAR) AS `price_base_type`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, CAST(t.`tva_tx` + AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, CAST(t.`localtax1_tx` + AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, CAST(t.`localtax2_tx` + AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`price_label` AS CHAR) AS `price_label`, CAST(t.`tosell` + AS CHAR) AS `tosell`, CAST(t.`price_by_qty` AS CHAR) AS `price_by_qty`, CAST(t.`fk_price_expression` + AS CHAR) AS `fk_price_expression`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_price` AS CHAR) + AS `multicurrency_price`, CAST(t.`multicurrency_price_ttc` AS CHAR) AS `multicurrency_price_ttc` + FROM `llx_product_price` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + label: + param: label + p__price_excl_vat: + literal: '1' + ref: + param: ref + p__vat_rate: + literal: '1' + pin__cost_price: + literal: '1' + pin__desiredstock: + literal: '1' + pin__entity: + literal: '1' + pin__fifo: + literal: '1' + pin__finished: + literal: '1' + pin__fk_barcode_type: + literal: '1' + pin__fk_country: + literal: '1' + pin__fk_default_bom: + literal: '1' + pin__fk_default_warehouse: + literal: '1' + pin__fk_default_workstation: + literal: '1' + pin__fk_parent: + literal: '1' + pin__fk_price_expression: + literal: '1' + pin__fk_product_type: + literal: '1' + pin__fk_project: + literal: '1' + pin__fk_state: + literal: '1' + pin__fk_unit: + literal: '1' + fk_user_author: + param: bot_user_id + fk_user_modif: + param: bot_user_id + pin__height: + literal: '1' + pin__height_units: + literal: '1' + pin__hidden: + literal: '1' + pin__length: + literal: '1' + pin__length_units: + literal: '1' + pin__lifetime: + literal: '1' + pin__lifo: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__mandatory_period: + literal: '1' + pin__net_measure: + literal: '1' + pin__net_measure_units: + literal: '1' + pin__pmp: + literal: '1' + pin__price_autogen: + literal: '1' + price_base_type: + literal: HT + pin__price_min: + literal: '1' + pin__price_min_ttc: + literal: '1' + pin__qc_frequency: + literal: '1' + pin__recuperableonly: + literal: '1' + pin__sell_or_eat_by_mandatory: + literal: '1' + pin__seuil_stock_alerte: + literal: '1' + pin__stock: + literal: '1' + pin__stockable_product: + literal: '1' + pin__surface: + literal: '1' + pin__surface_units: + literal: '1' + pin__tobatch: + literal: '1' + pin__tobuy: + literal: '1' + pin__tosell: + literal: '1' + pin__volume: + literal: '1' + pin__volume_units: + literal: '1' + pin__weight: + literal: '1' + pin__weight_units: + literal: '1' + pin__width: + literal: '1' + pin__width_units: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm + match: + code: + literal: AC_PRODUCT_CREATE + elementtype: + literal: product + entity: + literal: '1' + event_paid: + literal: '0' + fk_action: + literal: '40' + fk_parent: + literal: '0' + fk_user_action: + param: bot_user_id + fk_user_author: + param: bot_user_id + fulldayevent: + literal: '0' + percent: + literal: '-1' + priority: + literal: '0' + status: + literal: '0' + transparency: + literal: '0' + visibility: + literal: default + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources + match: + element_type: + literal: user + fk_element: + param: bot_user_id + mandatory: + literal: '0' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_product_price|1 row(s) the app adds to llx_product_price + match: + fk_user_author: + param: bot_user_id + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_product + mode: skip + reason: read in full, every column, by this contract's own surface + views: + llx_actioncomm: raw_llx_actioncomm + llx_actioncomm_resources: raw_llx_actioncomm_resources + llx_product_price: raw_llx_product_price + settle_s: 1 + settle_attempts: 5 + new_row_fields: + llx_actioncomm: + - datec + - datep + - datep2 + - durationp + - email_from + - email_msgid + - email_sender + - email_subject + - email_to + - email_tobcc + - email_tocc + - errors_to + - extraparams + - fk_element + - id + - import_key + - ip + - label + - location + - note + - recurid + - recurrule + - ref + - ref_ext + - reply_to + - tms + llx_actioncomm_resources: + - answer_status + - fk_actioncomm + - rowid + - transparency + llx_product_price: + - date_price + - default_vat_code + - entity + - fk_product + - import_key + - localtax1_tx + - localtax1_type + - localtax2_tx + - localtax2_type + - multicurrency_code + - multicurrency_tx + - price + - price_base_type + - price_by_qty + - price_label + - price_level + - price_min + - price_min_ttc + - price_ttc + - recuperableonly + - rowid + - tms + - tosell + - tva_tx + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml new file mode 100644 index 0000000000..9a555fc8e7 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml @@ -0,0 +1,104 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_product, found, fields label, price_excl_vat, ref, vat_rate. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` + AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, + CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_parent` AS CHAR) + AS `fk_parent`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS `description`, + CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`note` AS CHAR) AS `note`, CAST(t.`customcode` + AS CHAR) AS `customcode`, CAST(t.`fk_country` AS CHAR) AS `fk_country`, CAST(t.`fk_state` AS CHAR) + AS `fk_state`, CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, + CAST(t.`price_min` AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, + CAST(t.`price_base_type` AS CHAR) AS `price_base_type`, CAST(t.`price_label` AS CHAR) AS `price_label`, + CAST(t.`cost_price` AS CHAR) AS `cost_price`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, + CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, + CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, + CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, + CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, + CAST(t.`tosell` AS CHAR) AS `tosell`, CAST(t.`tobuy` AS CHAR) AS `tobuy`, CAST(t.`tobatch` AS + CHAR) AS `tobatch`, CAST(t.`sell_or_eat_by_mandatory` AS CHAR) AS `sell_or_eat_by_mandatory`, + CAST(t.`batch_mask` AS CHAR) AS `batch_mask`, CAST(t.`fk_product_type` AS CHAR) AS `fk_product_type`, + CAST(t.`duration` AS CHAR) AS `duration`, CAST(t.`seuil_stock_alerte` AS CHAR) AS `seuil_stock_alerte`, + CAST(t.`url` AS CHAR) AS `url`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`accountancy_code_sell` AS CHAR) AS `accountancy_code_sell`, + CAST(t.`accountancy_code_sell_intra` AS CHAR) AS `accountancy_code_sell_intra`, CAST(t.`accountancy_code_sell_export` + AS CHAR) AS `accountancy_code_sell_export`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`accountancy_code_buy_intra` AS CHAR) AS `accountancy_code_buy_intra`, CAST(t.`accountancy_code_buy_export` + AS CHAR) AS `accountancy_code_buy_export`, CAST(t.`partnumber` AS CHAR) AS `partnumber`, CAST(t.`net_measure` + AS CHAR) AS `net_measure`, CAST(t.`net_measure_units` AS CHAR) AS `net_measure_units`, CAST(t.`weight` + AS CHAR) AS `weight`, CAST(t.`weight_units` AS CHAR) AS `weight_units`, CAST(t.`length` AS CHAR) + AS `length`, CAST(t.`length_units` AS CHAR) AS `length_units`, CAST(t.`width` AS CHAR) AS `width`, + CAST(t.`width_units` AS CHAR) AS `width_units`, CAST(t.`height` AS CHAR) AS `height`, CAST(t.`height_units` + AS CHAR) AS `height_units`, CAST(t.`surface` AS CHAR) AS `surface`, CAST(t.`surface_units` AS + CHAR) AS `surface_units`, CAST(t.`volume` AS CHAR) AS `volume`, CAST(t.`volume_units` AS CHAR) + AS `volume_units`, CAST(t.`stockable_product` AS CHAR) AS `stockable_product`, CAST(t.`stock` + AS CHAR) AS `stock`, CAST(t.`pmp` AS CHAR) AS `pmp`, CAST(t.`fifo` AS CHAR) AS `fifo`, CAST(t.`lifo` + AS CHAR) AS `lifo`, CAST(t.`fk_default_warehouse` AS CHAR) AS `fk_default_warehouse`, CAST(t.`fk_default_bom` + AS CHAR) AS `fk_default_bom`, CAST(t.`fk_default_workstation` AS CHAR) AS `fk_default_workstation`, + CAST(t.`canvas` AS CHAR) AS `canvas`, CAST(t.`finished` AS CHAR) AS `finished`, CAST(t.`lifetime` + AS CHAR) AS `lifetime`, CAST(t.`qc_frequency` AS CHAR) AS `qc_frequency`, CAST(t.`hidden` AS CHAR) + AS `hidden`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, + CAST(t.`fk_price_expression` AS CHAR) AS `fk_price_expression`, CAST(t.`desiredstock` AS CHAR) + AS `desiredstock`, CAST(t.`fk_unit` AS CHAR) AS `fk_unit`, CAST(t.`price_autogen` AS CHAR) AS + `price_autogen`, CAST(t.`fk_project` AS CHAR) AS `fk_project`, CAST(t.`mandatory_period` AS CHAR) + AS `mandatory_period`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CASE WHEN CAST(%(v0)s + AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `p__price_excl_vat`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate` FROM `llx_product` + t + sql_query_params: + v0: + param: price_excl_vat + v1: + param: vat_rate +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + label: + param: label + p__price_excl_vat: + literal: '1' + ref: + param: ref + p__vat_rate: + literal: '1' + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_product + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + llx_societe: + - fk_user_modif + - socialnetworks + - tms + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml new file mode 100644 index 0000000000..0536988a03 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml @@ -0,0 +1,101 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_customer, found, fields customer, new_address, new_email, new_phone, town. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + nom: + param: customer + address: + param: new_address + email: + param: new_email + phone: + param: new_phone + kind: phone + town: + param: town + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml new file mode 100644 index 0000000000..788c0c1431 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml @@ -0,0 +1,218 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_customer, update, fields customer, new_address, new_email, new_phone, town. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` + t + raw_llx_actioncomm: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` + AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, + CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` + AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` + AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS + `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, + CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, + CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` + AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) + AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, + CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` + AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` + AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` + AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS + CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) + AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, + CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` + AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS + `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` + AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` + AS CHAR) AS `extraparams` FROM `llx_actioncomm` t + raw_llx_actioncomm_resources: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` + AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` + AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` + AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + nom: + param: customer + address: + param: new_address + email: + param: new_email + phone: + param: new_phone + kind: phone + town: + param: town + expected_count: 1 +- kind: record_written + probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm + match: + entity: + literal: '1' + event_paid: + literal: '0' + fk_action: + literal: '40' + fk_parent: + literal: '0' + fk_user_action: + param: bot_user_id + fk_user_author: + param: bot_user_id + fulldayevent: + literal: '0' + percent: + literal: '-1' + priority: + literal: '0' + status: + literal: '0' + transparency: + literal: '0' + visibility: + literal: default + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources + match: + element_type: + literal: user + fk_element: + param: bot_user_id + mandatory: + literal: '0' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: + - address + - email + - phone + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: + llx_actioncomm: raw_llx_actioncomm + llx_actioncomm_resources: raw_llx_actioncomm_resources + settle_s: 1 + settle_attempts: 5 + new_row_fields: + llx_actioncomm: + - code + - datec + - datep + - datep2 + - durationp + - elementtype + - email_from + - email_msgid + - email_sender + - email_subject + - email_to + - email_tobcc + - email_tocc + - errors_to + - extraparams + - fk_soc + - id + - import_key + - ip + - label + - location + - note + - recurid + - recurrule + - ref + - ref_ext + - reply_to + - tms + llx_actioncomm_resources: + - answer_status + - fk_actioncomm + - rowid + - transparency + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml new file mode 100644 index 0000000000..2d06c7591d --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml @@ -0,0 +1,99 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_customer, found, fields customer, new_email, new_phone, town. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + nom: + param: customer + email: + param: new_email + phone: + param: new_phone + kind: phone + town: + param: town + expected_count: 1 +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: {} + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml new file mode 100644 index 0000000000..6d51c78306 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml @@ -0,0 +1,215 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_customer, update, fields customer, new_email, new_phone, town. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-dolidb +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + database: dolidb + host: 127.0.0.1 + port: 9616 + user: fcg_reader + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` + AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` + AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, + CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) + AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, + CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS + CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, + CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) + AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS + `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) + AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS + `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` + AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, + CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, + CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, + CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` + AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` + AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS + `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, + CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, + CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, + CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` + AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` + AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` + AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` + AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` + AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` + AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` + AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` + AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, + CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` + AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` + AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` + AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` + AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` + AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` + AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` + AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` + AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) + AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) + AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` + AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, + CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS + CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` + t + raw_llx_actioncomm: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` + AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, + CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` + AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` + AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS + `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, + CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, + CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` + AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) + AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, + CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` + AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` + AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` + AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS + CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) + AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, + CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` + AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS + `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` + AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` + AS CHAR) AS `extraparams` FROM `llx_actioncomm` t + raw_llx_actioncomm_resources: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` + AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` + AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` + AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` + t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + nom: + param: customer + email: + param: new_email + phone: + param: new_phone + kind: phone + town: + param: town + expected_count: 1 +- kind: record_written + probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm + match: + entity: + literal: '1' + event_paid: + literal: '0' + fk_action: + literal: '40' + fk_parent: + literal: '0' + fk_user_action: + param: bot_user_id + fk_user_author: + param: bot_user_id + fulldayevent: + literal: '0' + percent: + literal: '-1' + priority: + literal: '0' + status: + literal: '0' + transparency: + literal: '0' + visibility: + literal: default + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources + match: + element_type: + literal: user + fk_element: + param: bot_user_id + mandatory: + literal: '0' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - fk_user_modif + - socialnetworks + - tms + target_fields: + - email + - phone + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + allow_tables: + - table: llx_societe + mode: skip + reason: read in full, every column, by this contract's own surface + views: + llx_actioncomm: raw_llx_actioncomm + llx_actioncomm_resources: raw_llx_actioncomm_resources + settle_s: 1 + settle_attempts: 5 + new_row_fields: + llx_actioncomm: + - code + - datec + - datep + - datep2 + - durationp + - elementtype + - email_from + - email_msgid + - email_sender + - email_subject + - email_to + - email_tobcc + - email_tocc + - errors_to + - extraparams + - fk_soc + - id + - import_key + - ip + - label + - location + - note + - recurid + - recurrule + - ref + - ref_ext + - reply_to + - tms + llx_actioncomm_resources: + - answer_status + - fk_actioncomm + - rowid + - transparency + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml new file mode 100644 index 0000000000..be7c828bd3 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml @@ -0,0 +1,311 @@ +schema_version: openadapt.contract-manifest/v1 +dialect: mysql +database: dolidb +actor_params: +- bot_user_id +questions_open: [] +kinds: + create_customer: + mode: create + main_table: llx_societe + parameters: + - name + - address + - zip + - town + - country + - phone + - email + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - address + - country + - email + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml + found: contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml + create_product: + mode: create + main_table: llx_product + parameters: + - ref + - label + - price_excl_vat + - vat_rate + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - label + - price_excl_vat + - ref + - vat_rate + lists: {} + dispatch_value: null + create: contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml + found: contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml + create_draft_invoice: + mode: create + main_table: llx_facture + parameters: + - customer + - town + - invoice_date + - lines + identity: [] + lists: + - lines + dispatch: null + variants: + - fields: + - customer + - invoice_date + - town + lists: + lines: 1 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx1.678821c0.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx1.678821c0.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 2 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 3 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx3.2f437a2c.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx3.2f437a2c.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 4 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx4.aea56f08.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx4.aea56f08.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 5 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx5.58afdd66.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx5.58afdd66.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 6 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx6.edf87893.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx6.edf87893.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 7 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx7.086ca1eb.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx7.086ca1eb.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 8 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx8.8d4e9b1f.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx8.8d4e9b1f.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 9 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx9.042138e5.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx9.042138e5.found.yaml + - fields: + - customer + - invoice_date + - town + lists: + lines: 10 + dispatch_value: null + create: contracts/create_draft_invoice/customer+invoice_date+town+linesx10.f6fa907c.create.yaml + found: contracts/create_draft_invoice/customer+invoice_date+town+linesx10.f6fa907c.found.yaml + update_customer: + mode: update + main_table: llx_societe + parameters: + - customer + - town + - new_phone + - new_email + - new_address + - new_zip + identity: + - customer + - town + lists: [] + dispatch: null + variants: + - fields: + - customer + - new_phone + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_phone+town.e481e632.update.yaml + found: contracts/update_customer/customer+new_phone+town.e481e632.found.yaml + - fields: + - customer + - new_email + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_email+town.014f1cf4.update.yaml + found: contracts/update_customer/customer+new_email+town.014f1cf4.found.yaml + - fields: + - customer + - new_address + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+town.7b5a4e67.update.yaml + found: contracts/update_customer/customer+new_address+town.7b5a4e67.found.yaml + - fields: + - customer + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_zip+town.5089de78.update.yaml + found: contracts/update_customer/customer+new_zip+town.5089de78.found.yaml + - fields: + - customer + - new_email + - new_phone + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml + found: contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml + - fields: + - customer + - new_address + - new_phone + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_phone+town.be3325cd.update.yaml + found: contracts/update_customer/customer+new_address+new_phone+town.be3325cd.found.yaml + - fields: + - customer + - new_phone + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_phone+new_zip+town.00a25668.update.yaml + found: contracts/update_customer/customer+new_phone+new_zip+town.00a25668.found.yaml + - fields: + - customer + - new_address + - new_email + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_email+town.304fdd4f.update.yaml + found: contracts/update_customer/customer+new_address+new_email+town.304fdd4f.found.yaml + - fields: + - customer + - new_email + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_email+new_zip+town.adf5982d.update.yaml + found: contracts/update_customer/customer+new_email+new_zip+town.adf5982d.found.yaml + - fields: + - customer + - new_address + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_zip+town.8d26c1c7.update.yaml + found: contracts/update_customer/customer+new_address+new_zip+town.8d26c1c7.found.yaml + - fields: + - customer + - new_address + - new_email + - new_phone + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml + found: contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml + - fields: + - customer + - new_email + - new_phone + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_email+new_phone+new_zip+town.d9b5d1c2.update.yaml + found: contracts/update_customer/customer+new_email+new_phone+new_zip+town.d9b5d1c2.found.yaml + - fields: + - customer + - new_address + - new_phone + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_phone+new_zip+town.998004a8.update.yaml + found: contracts/update_customer/customer+new_address+new_phone+new_zip+town.998004a8.found.yaml + - fields: + - customer + - new_address + - new_email + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_email+new_zip+town.e4f14417.update.yaml + found: contracts/update_customer/customer+new_address+new_email+new_zip+town.e4f14417.found.yaml + - fields: + - customer + - new_address + - new_email + - new_phone + - new_zip + - town + lists: {} + dispatch_value: null + update: contracts/update_customer/customer+new_address+new_email+new_phone+new_zip+town.0d2342b6.update.yaml + found: contracts/update_customer/customer+new_address+new_email+new_phone+new_zip+town.0d2342b6.found.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json new file mode 100644 index 0000000000..be5ce10a31 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json @@ -0,0 +1,157 @@ +{ + "applied": [], + "dialect": "mysql", + "tables": 387, + "questions": [ + { + "number": 1, + "key": "bookkeeping", + "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", + "proposal": "accept", + "answered": true + }, + { + "number": 2, + "key": "recheck_after_s", + "text": "How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `.", + "proposal": 300, + "answered": true + }, + { + "number": 3, + "key": "locations.create_draft_invoice", + "text": "Kind `create_draft_invoice`: `invoice_date` is in several places: [0] llx_facture.datef (date); [1] llx_facture.date_lim_reglement (date) Answer `locations.create_draft_invoice: {: }` to pick one.", + "proposal": null, + "answered": true + } + ], + "kinds": { + "create_customer": { + "mode": "create", + "main_table": "llx_societe", + "locations": { + "address": "llx_societe.address (exact)", + "country": "llx_societe.fk_pays -> llx_c_country.label (exact)", + "email": "llx_societe.email (exact)", + "name": "llx_societe.nom (exact)", + "phone": "llx_societe.phone (phone)", + "town": "llx_societe.town (exact)", + "zip": "llx_societe.zip (exact)" + }, + "placed_by_name": [], + "demonstrations": 2, + "children": {}, + "lists": {}, + "side_rows": [ + { + "table": "llx_actioncomm", + "count": 1, + "unless": null + }, + { + "table": "llx_actioncomm_resources", + "count": 1, + "unless": null + } + ], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 1, + "rejected": [] + }, + "create_product": { + "mode": "create", + "main_table": "llx_product", + "locations": { + "label": "llx_product.label (exact)", + "price_excl_vat": "llx_product.price (number)", + "ref": "llx_product.ref (exact)", + "vat_rate": "llx_product.tva_tx (number)" + }, + "placed_by_name": [], + "demonstrations": 2, + "children": {}, + "lists": {}, + "side_rows": [ + { + "table": "llx_actioncomm", + "count": 1, + "unless": null + }, + { + "table": "llx_actioncomm_resources", + "count": 1, + "unless": null + }, + { + "table": "llx_product_price", + "count": 1, + "unless": null + } + ], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 1, + "rejected": [] + }, + "create_draft_invoice": { + "mode": "create", + "main_table": "llx_facture", + "locations": { + "customer": "llx_facture.fk_soc -> llx_societe.nom (exact)", + "invoice_date": "llx_facture.datef (date)", + "town": "llx_facture.fk_soc -> llx_societe.town (exact)" + }, + "placed_by_name": [], + "demonstrations": 2, + "children": {}, + "lists": { + "lines": "llx_facturedet" + }, + "side_rows": [], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 10, + "rejected": [] + }, + "update_customer": { + "mode": "update", + "main_table": "llx_societe", + "locations": { + "customer": "llx_societe.nom (exact)", + "new_address": "llx_societe.address (exact)", + "new_email": "llx_societe.email (exact)", + "new_phone": "llx_societe.phone (phone)", + "new_zip": "llx_societe.zip (exact)", + "town": "llx_societe.town (exact)" + }, + "placed_by_name": [ + "new_address", + "new_zip" + ], + "demonstrations": 2, + "children": {}, + "lists": {}, + "side_rows": [ + { + "table": "llx_actioncomm", + "count": 1, + "unless": null + }, + { + "table": "llx_actioncomm_resources", + "count": 1, + "unless": null + } + ], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 15, + "rejected": [] + } + } +} \ No newline at end of file diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 58632ec7ab..a6f4ce993d 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1791,8 +1791,21 @@ def propose( book.setdefault(t, set()).update(cols) for t, cols in dict(ans.get("columns_remove", {})).items(): book[t] = book.get(t, set()) - set(cols) - if logs or book or unchecked: + sides = sorted( + {f"{s.table} ({s.count} per write)" for p in plans for s in p.sides} + ) + for p in plans: # a table the person calls a log is no longer counted + p.sides = [s for s in p.sides if s.table not in logs] + if logs or book or unchecked or sides: lines = [] + if sides: + lines.append( + "Rows the app adds beside each write, expected exactly that many " + "times (a second save adds a second one, which stops the write; " + "list a table under logs_add to allow any number): " + + ", ".join(sides) + + "." + ) if logs: lines.append( "Tables treated as the app's own logs (rows may be added, existing " From faf64ce28351e073df127631f8bb6b102f4d97ff Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:32:49 -0400 Subject: [PATCH 50/94] WIP: empty child rows, locations shared across kinds, exact-name update fields --- .../generate_contracts/demo_redmine.py | 4 +- .../generate/verify_contract.py | 84 +++++++++++++++++-- .../tests/test_generate_verify_contract.py | 7 +- 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/packages/flow/benchmark/generate_contracts/demo_redmine.py b/packages/flow/benchmark/generate_contracts/demo_redmine.py index 6f643913ad..e98b53298a 100644 --- a/packages/flow/benchmark/generate_contracts/demo_redmine.py +++ b/packages/flow/benchmark/generate_contracts/demo_redmine.py @@ -111,7 +111,9 @@ def create_payload(v: dict) -> dict: if kind == "ensure_issue" and int(one( "SELECT COUNT(*) FROM issues i JOIN projects p ON p.id=i.project_id " "WHERE p.name=%s AND i.subject=%s", (v["project"], v["subject"]))): # fmt: skip - continue + # Already there: demonstrate a create with values of our own. + v = {**v, "subject": "Generator demo: rotate the backup keys", + "priority": "Low", "assignee": "Jon Park"} demo( work, kind, diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index a6f4ce993d..7595f8beee 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -191,6 +191,9 @@ class ChildGroup: #: row also holds the title): with one of them given and this one not, #: the row is still expected, with this value empty. co_owners: list[str] = field(default_factory=list) + #: The app adds this row with an empty value when the task leaves the + #: parameter out (Redmine's custom field values): expected then too. + empty_when_null: bool = False @dataclass @@ -232,6 +235,8 @@ class KindPlan: removed: dict[str, int] = field(default_factory=dict) #: Parameters placed by name, not by a demonstration. by_name: list[str] = field(default_factory=list) + #: Parameters placed where another kind's demonstrations found them. + shared: list[str] = field(default_factory=list) def _scalars(values: Mapping[str, Any]) -> dict[str, Any]: @@ -398,6 +403,37 @@ def analyze( return plan +def _share_locations(schema: Schema, plans: Sequence[KindPlan], kinds: Sequence[KindSpec]) -> None: + """An update parameter no example of its kind gives (a priority), placed + where another kind's demonstrations found the parameter of the same name + on the same main table (``issues.priority_id -> enumerations.name``). + Placed this way, it replaces a placement by name.""" + + for plan in plans: + if plan.mode != "update" or not plan.anchor: + continue + for name in plan.params: + if name in plan.identity or name == plan.dispatch: + continue + if name in plan.locations and name not in plan.by_name: + continue + for other in plans: + if other is plan or other.anchor != plan.anchor: + continue + loc = other.locations.get(name) + if loc is None or name in other.by_name: + continue + if loc.hops and loc.hops[0].kind != "fk": + continue + plan.locations[name] = loc + if name in plan.by_name: + plan.by_name.remove(name) + plan.shared.append(name) + if name not in plan.changes: + plan.changes.append(name) + break + + def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: """An update parameter no demonstration gives (``new_zip``), placed at the main table's column of the same name after a leading ``new_`` or @@ -408,8 +444,6 @@ def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: if plan.mode != "update" or not plan.anchor: return None name = re.sub(r"^(new|set)_", "", param) - if name == param: - return None col = schema.tables[plan.anchor].column(name) if col is None or name in schema.tables[plan.anchor].primary_key: return None @@ -603,6 +637,8 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N per_table.setdefault(ch.table, []) counts.setdefault(ch.table, [0] * len(plan.demos)) continue + if ch.table in child_tables and _is_empty_param_row(schema, plan, d, a, ch): + continue if _is_ref_row(schema, plan, d, ch): continue per_table.setdefault(ch.table, []).append(ch.after or {}) @@ -660,6 +696,33 @@ def _is_param_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> boo return False +def _is_empty_param_row( + schema: Schema, plan: KindPlan, d: Demo, a: RowChange, ch: RowChange +) -> bool: + """A row the app added for a parameter the task left out: linked to the + main record, holding every constant of that parameter's rows, and an + empty value where the parameter's value goes.""" + + if ch.after is None or a.after is None: + return False + for p, g in plan.children.items(): + if g.table != ch.table or d.values.get(p) is not None: + continue + if ch.after.get(g.link.column) != a.after.get(g.link.ref_column): + continue + rel = g.params[p] + cols = rel.columns if not rel.hops else (rel.hops[0].column,) + if any(ch.after.get(c) not in (None, "") for c in cols): + continue + consts = [ + (c, r.value) for c, r in g.roles.items() if r.role == "constant" + ] + if consts and all(ch.after.get(c) == v for c, v in consts): + g.empty_when_null = True + return True + return False + + def _is_ref_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: """A row the write added that a parameter's location points to (a new email address row a link row names): declared by the value it holds.""" @@ -985,7 +1048,9 @@ def build_contract( groups = [ (p, g) for p, g in plan.children.items() - if p in fields or (plan.mode == "create" and set(g.co_owners) & fields) + if p in fields + or g.empty_when_null + or (plan.mode == "create" and set(g.co_owners) & fields) ] groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] for p, g in groups: @@ -1455,11 +1520,14 @@ def flatten(values: Mapping[str, Any]) -> dict[str, Any]: return out -def sample_values(plan: KindPlan, variant: Variant) -> dict[str, Any]: - """Values to dry-read a contract with: the demonstration's own.""" +def sample_values( + plan: KindPlan, variant: Variant, others: Sequence[Demo] = () +) -> dict[str, Any]: + """Values to dry-read a contract with: the demonstrations' own (another + kind's, for a parameter placed from that kind).""" out: dict[str, Any] = {} - for d in plan.demos: + for d in [*plan.demos, *others]: for k, v in d.values.items(): if v is not None and k not in out: out[k] = v @@ -1731,6 +1799,7 @@ def propose( plans = [analyze(schema, k, demos, actor_params) for k in kinds] questions: list[Question] = [] + _share_locations(schema, plans, kinds) location_questions: list[Question] = [] # Q: per kind, a value the generator couldn't place. for p in plans: @@ -1951,6 +2020,7 @@ def propose( k: v.describe(p.anchor) for k, v in sorted(p.locations.items()) }, "placed_by_name": sorted(p.by_name), + "placed_from_another_kind": sorted(p.shared), "demonstrations": len(p.demos), "children": {k: g.table for k, g in p.children.items()}, "lists": {k: g.table for k, g in p.lists.items()}, @@ -2009,7 +2079,7 @@ def propose( why = why or guard_problem(s["sql_query"]) if why is None and validate: why = dry_read( - path, flatten(sample_values(p, v)), dialect == "mysql" + path, flatten(sample_values(p, v, demos)), dialect == "mysql" ) if why is not None: path.unlink() diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index d3cf81c4b4..20475c3bf8 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -334,9 +334,10 @@ def test_questions_and_contracts(generated: dict[str, Any]) -> None: assert kinds["update_issue"]["mode"] == "update" assert kinds["create_issue"]["emitted"] > 0, kinds["create_issue"] # Four change fields the examples give (status, assignee, done ratio, - # notes): every non-empty subset gets a contract. No example gives a due - # date, so no contract changes one. - assert kinds["update_issue"]["emitted"] == 15, json.dumps( + # notes), and the due date that create_issue's demonstrations placed on + # the same table: every non-empty subset gets a contract. + assert kinds["update_issue"]["placed_from_another_kind"] == ["due_date"] + assert kinds["update_issue"]["emitted"] == 31, json.dumps( kinds["update_issue"], indent=1 ) assert "journal_details" in kinds["update_issue"]["logs"] From 7a38647dd911060d2c09974912b3c10cdc71595d Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:34:00 -0400 Subject: [PATCH 51/94] WIP: typed sample values for fields placed by name --- packages/flow/openadapt_flow/generate/verify_contract.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 7595f8beee..65b777989c 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1537,6 +1537,13 @@ def sample_values( items = out.get(p) if isinstance(out.get(p), list) else [] if items: out[p] = [items[i % len(items)] for i in range(n)] + for k in variant.fields: + loc = plan.locations.get(k) + if k not in out and loc is not None: + # Placed by name, so no demonstration gave a value: any value of + # the right type reads the same columns. + out[k] = {"number": 1, "minutes": 1, "date": "2026-01-01", + "datetime": "2026-01-01 00:00"}.get(loc.rule, "x") # fmt: skip keep = set(variant.fields) | set(variant.lists) return {k: v for k, v in out.items() if k in keep} From 0837f30ae162eeceb0d5079891d65f87315abee5 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 14:45:40 -0400 Subject: [PATCH 52/94] WIP: nullable fields optional on create; date parts unchecked; Postgres bookkeeping ignored on every surface --- .../openadapt_flow/generate/demo_analysis.py | 33 +++++++++++++++++++ .../generate/verify_contract.py | 25 ++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 02d8bdba7d..50283d7dd5 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -589,6 +589,10 @@ def column_roles( ): out[name] = ColumnRole("varies", detail={"why": "holds the row's own key"}) continue + part = _date_part(values, [d for d, _ in rows]) + if part is not None: + out[name] = ColumnRole("varies", detail={"why": f"the {part} of a date the task gives"}) + continue copied = _copied(schema, t, name, rows) if copied is not None: out[name] = ColumnRole("copied", detail=copied) @@ -618,6 +622,35 @@ def column_roles( return out +def _date_part(values: Sequence[Optional[str]], demos: Sequence["Demo"]) -> Optional[str]: + """Is the column the year, month, ISO week or day of one date value the + task gives, in every row? (Redmine stores a time entry's ``tyear``, + ``tmonth`` and ``tweek``.) The app computes it, so it isn't pinned.""" + + parts = { + "year": lambda d: d.year, + "month": lambda d: d.month, + "week": lambda d: d.isocalendar()[1], + "day": lambda d: d.day, + } + for label, fn in parts.items(): + ok = bool(values) + for v, demo in zip(values, demos): + dates = [] + for x in demo.values.values(): + if isinstance(x, str) and re.match(r"^\d{4}-\d{2}-\d{2}", x): + try: + dates.append(_dt.date.fromisoformat(x[:10])) + except ValueError: + pass + if v is None or not any(str(fn(dd)) == v for dd in dates): + ok = False + break + if ok: + return label + return None + + def _copied( schema: Schema, t: Table, diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 65b777989c..95912773e3 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -51,6 +51,7 @@ Demo, Hop, Location, + _hop_cost, _rank, best, column_roles, @@ -393,6 +394,20 @@ def analyze( if plan.mode == "update" and name not in plan.identity: plan.changes.append(name) progress = True + if plan.mode == "create": + # A create task may leave out any value whose place can be empty: a + # nullable column, a nullable key column, or a row beside the record. + anchor_t = schema.tables[plan.anchor] + for p, loc in plan.locations.items(): + col = None + if not loc.hops and len(loc.columns) == 1: + col = anchor_t.column(loc.columns[0]) + elif loc.hops and loc.hops[0].kind == "fk": + col = anchor_t.column(loc.hops[0].column) + if (col is not None and col.nullable) or (loc.hops and loc.hops[0].kind == "child"): + if p not in plan.optional: + plan.optional.append(p) + plan.optional.sort() if plan.mode == "update" and not plan.identity: plan.refused.append( "no parameter names the record before the write, so the update's " @@ -441,7 +456,7 @@ def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: type. Reported as placed by name; a demonstration that gives it replaces this.""" - if plan.mode != "update" or not plan.anchor: + if not plan.anchor: return None name = re.sub(r"^(new|set)_", "", param) col = schema.tables[plan.anchor].column(name) @@ -498,7 +513,7 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: if d.values.get(q) is not None and not isinstance(d.values.get(q), (list, dict)) ] - if hits and all(h and h[0].hops for h in hits): + if hits and all(h and h[0].hops and _hop_cost(h[0].hops[0]) <= 1 for h in hits): co.append(q) explained |= {h[0].hops[0].column for h in hits} plan.children[p] = ChildGroup( @@ -1167,7 +1182,11 @@ def build_contract( # against them, and the whole-database frame covers every other table. ignore = sorted( {c for cols in bookkeeping.values() for c in cols} - & _all_columns(schema, named_tables) + & ( + _all_columns(schema, named_tables) + if schema.dialect == "mysql" + else _all_columns(schema, set(schema.tables)) + ) ) frame: dict[str, Any] = {} if ignore: From 7033e3971287f46bfa86352ce53f166ebdf13272 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:03:27 -0400 Subject: [PATCH 53/94] WIP: found contracts check rows beside the record; unique pin columns --- .../generate/verify_contract.py | 99 ++++++++++++------- 1 file changed, 65 insertions(+), 34 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 95912773e3..ed25c8e436 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -404,7 +404,9 @@ def analyze( col = anchor_t.column(loc.columns[0]) elif loc.hops and loc.hops[0].kind == "fk": col = anchor_t.column(loc.hops[0].column) - if (col is not None and col.nullable) or (loc.hops and loc.hops[0].kind == "child"): + if (col is not None and col.nullable) or ( + loc.hops and loc.hops[0].kind == "child" + ): if p not in plan.optional: plan.optional.append(p) plan.optional.sort() @@ -418,7 +420,9 @@ def analyze( return plan -def _share_locations(schema: Schema, plans: Sequence[KindPlan], kinds: Sequence[KindSpec]) -> None: +def _share_locations( + schema: Schema, plans: Sequence[KindPlan], kinds: Sequence[KindSpec] +) -> None: """An update parameter no example of its kind gives (a priority), placed where another kind's demonstrations found the parameter of the same name on the same main table (``issues.priority_id -> enumerations.name``). @@ -513,7 +517,9 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: if d.values.get(q) is not None and not isinstance(d.values.get(q), (list, dict)) ] - if hits and all(h and h[0].hops and _hop_cost(h[0].hops[0]) <= 1 for h in hits): + if hits and all( + h and h[0].hops and _hop_cost(h[0].hops[0]) <= 1 for h in hits + ): co.append(q) explained |= {h[0].hops[0].column for h in hits} plan.children[p] = ChildGroup( @@ -729,9 +735,7 @@ def _is_empty_param_row( cols = rel.columns if not rel.hops else (rel.hops[0].column,) if any(ch.after.get(c) not in (None, "") for c in cols): continue - consts = [ - (c, r.value) for c, r in g.roles.items() if r.role == "constant" - ] + consts = [(c, r.value) for c, r in g.roles.items() if r.role == "constant"] if consts and all(ch.after.get(c) == v for c, v in consts): g.empty_when_null = True return True @@ -1059,13 +1063,14 @@ def build_contract( # Rows beside the main record that hold a value (a note, a custom field, # an email address link), and list items (invoice lines). - if flavor != "found": + found = flavor == "found" + if True: groups = [ (p, g) for p, g in plan.children.items() if p in fields - or g.empty_when_null - or (plan.mode == "create" and set(g.co_owners) & fields) + or (not found and g.empty_when_null) + or (not found and plan.mode == "create" and set(g.co_owners) & fields) ] groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] for p, g in groups: @@ -1117,23 +1122,24 @@ def build_contract( else: m[key] = _value_expr(pname, loc.rule) for col, role in sorted(g.roles.items()): - if role.role == "link": + if role.role == "link" or found: continue _pin(b, surf, g.table, col, role, m, settings.actor_params) - effects.append( - { - "kind": "record_written", - "probe": f"surface={sname}|a row beside the main record holds {p}", - "match": m, - "expected_count": 1, - "count_new_only": True, - } - ) + effect = { + "kind": "record_written", + "probe": f"surface={sname}|a row beside the main record holds {p}", + "match": m, + "expected_count": 1, + } + if not found: + # Found: the row is already there, and nothing changes. + effect["count_new_only"] = True + effects.append(effect) if plan.mode == "update": target_fields.update(g.changed_columns) # A new row a link row names (a new email address). for p, ref in plan.added_refs.items(): - if p not in fields: + if p not in fields or found: continue effects.append( _raw_effect( @@ -1149,7 +1155,7 @@ def build_contract( ) ) for side in plan.sides: - if side.unless is not None and side.unless in fields: + if found or (side.unless is not None and side.unless in fields): continue m = {} for col, role in sorted(side.roles.items()): @@ -1301,17 +1307,34 @@ def _pin( if role.value is not None and column is not None and column.kind == "text": match.setdefault(col, {"literal": role.value}) return - alias = f"pin__{_alias(col)}" - surf.columns.append((alias, b.pin("t", table, col, role.value))) + alias = _add_column( + surf, f"pin__{_alias(col)}", b.pin("t", table, col, role.value) + ) match[alias] = {"literal": "1"} elif role.role == "actor": match.setdefault(col, {"param": role.detail["param"]}) elif role.role == "copied": - alias = f"pin__{_alias(col)}" - surf.columns.append((alias, b.copied("t", role.detail, table, col))) + alias = _add_column( + surf, f"pin__{_alias(col)}", b.copied("t", role.detail, table, col) + ) match[alias] = {"literal": "1"} +def _add_column(surf: Surface, alias: str, expr: str) -> str: + """Add a computed column; the same name with another expression (two + rows of one table pinned to different constants) gets its own name.""" + + for a, e in surf.columns: + if a == alias and e == expr: + return alias + if any(a == alias for a, _ in surf.columns): + alias = f"{alias}__{hashlib.sha256(expr.encode()).hexdigest()[:6]}" + if any(a == alias for a, _ in surf.columns): + return alias + surf.columns.append((alias, expr)) + return alias + + def _raw_effect( b: SqlBuilder, schema: Schema, @@ -1886,9 +1909,7 @@ def propose( book.setdefault(t, set()).update(cols) for t, cols in dict(ans.get("columns_remove", {})).items(): book[t] = book.get(t, set()) - set(cols) - sides = sorted( - {f"{s.table} ({s.count} per write)" for p in plans for s in p.sides} - ) + sides = sorted({f"{s.table} ({s.count} per write)" for p in plans for s in p.sides}) for p in plans: # a table the person calls a log is no longer counted p.sides = [s for s in p.sides if s.table not in logs] if logs or book or unchecked or sides: @@ -2243,14 +2264,24 @@ def _exists(contract: Any, params: Mapping[str, Any]) -> bool: from openadapt_flow.runtime.effects.effect import record_matches from openadapt_flow.verify_run import _build_verifier + from openadapt_flow.runtime.effects.surfaces import surface_of + verifier = _build_verifier(contract.config, params) state = verifier.capture_pre_state() - effect = contract.config.effects[0].resolve(params) - sub = (state.detail or {}).get("record", {}) - if not sub.get("reachable"): - raise GenerateError("the store could not be read to choose the contract") - selector = {k: v for k, v in effect.match.items()} - return any(record_matches(dict(r), selector) for r in sub.get("records", [])) + # Every effect of the found contract (the main record, and each row + # beside it that holds a value) must already hold. + for effect in contract.config.effects: + bound = effect.resolve(params) + sub = (state.detail or {}).get(surface_of(effect) or "record", {}) + if not sub.get("reachable"): + raise GenerateError("the store could not be read to choose the contract") + selector = dict(bound.match) + hits = sum( + 1 for r in sub.get("records", []) if record_matches(dict(r), selector) + ) + if hits != bound.expected_count: + return False + return True # -- CLI ------------------------------------------------------------------------------------ From 1098a2fea7cca1818c1159d1ea55ba8ef6c750f9 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:06:47 -0400 Subject: [PATCH 54/94] WIP: tidy found handling --- .../openadapt_flow/generate/demo_analysis.py | 8 +- .../generate/verify_contract.py | 220 +++++++++--------- 2 files changed, 114 insertions(+), 114 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 50283d7dd5..c3c3b9bfdb 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -591,7 +591,9 @@ def column_roles( continue part = _date_part(values, [d for d, _ in rows]) if part is not None: - out[name] = ColumnRole("varies", detail={"why": f"the {part} of a date the task gives"}) + out[name] = ColumnRole( + "varies", detail={"why": f"the {part} of a date the task gives"} + ) continue copied = _copied(schema, t, name, rows) if copied is not None: @@ -622,7 +624,9 @@ def column_roles( return out -def _date_part(values: Sequence[Optional[str]], demos: Sequence["Demo"]) -> Optional[str]: +def _date_part( + values: Sequence[Optional[str]], demos: Sequence["Demo"] +) -> Optional[str]: """Is the column the year, month, ISO week or day of one date value the task gives, in every row? (Redmine stores a time entry's ``tyear``, ``tmonth`` and ``tweek``.) The app computes it, so it isn't pinned.""" diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index ed25c8e436..5e546457f9 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1064,118 +1064,114 @@ def build_contract( # Rows beside the main record that hold a value (a note, a custom field, # an email address link), and list items (invoice lines). found = flavor == "found" - if True: - groups = [ - (p, g) - for p, g in plan.children.items() - if p in fields - or (not found and g.empty_when_null) - or (not found and plan.mode == "create" and set(g.co_owners) & fields) - ] - groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] - for p, g in groups: - name = f"rows:{g.table}" - sname = _alias(name) - if sname not in surfaces: - cols = _base_columns(b, g.table) - link = g.link - # The main record's values, seen from the child row. - for ip in identity_params: - loc = locations[ip] - up = Location( - (Hop("fk", anchor, link.column, link.ref_column, True),) - + loc.hops, - loc.columns, - loc.rule, - ) - expr = b.path("t", g.table, up) - if loc.rule in ("number", "date", "datetime", "minutes"): - expr = b.flag(expr, loc.rule, b.ph(ip)) - cols.append((f"main__{_alias(ip)}", expr)) - surfaces[sname] = Surface( - sname, settings.views.get(g.table, g.table), cols - ) - named_tables.add(g.table) - surf = surfaces[sname] - items = range(variant.lists[p]) if g.list_param else [None] - for i in items: - m: dict[str, Any] = {} - for ip in identity_params: - loc = locations[ip] - if loc.rule in ("number", "date", "datetime", "minutes"): - m[f"main__{_alias(ip)}"] = {"literal": "1"} - else: - m[f"main__{_alias(ip)}"] = _value_expr(ip, loc.rule) - for k, loc in g.params.items(): - pname = k if g.list_param is None else f"{p}.{i}.{k}" - if g.list_param is None and pname not in fields: - # Given another owner but not this value: empty. - for c in loc.columns if not loc.hops else [loc.hops[0].column]: - alias = f"empty__{_alias(c)}" - if alias not in {a for a, _ in surf.columns}: - surf.columns.append((alias, b.empty("t", g.table, c))) - m[alias] = {"literal": "1"} - continue - key = _place(b, surf, g.table, pname, loc) - if loc.rule in ("number", "date", "datetime", "minutes"): - m[key] = {"literal": "1"} - else: - m[key] = _value_expr(pname, loc.rule) - for col, role in sorted(g.roles.items()): - if role.role == "link" or found: - continue - _pin(b, surf, g.table, col, role, m, settings.actor_params) - effect = { - "kind": "record_written", - "probe": f"surface={sname}|a row beside the main record holds {p}", - "match": m, - "expected_count": 1, - } - if not found: - # Found: the row is already there, and nothing changes. - effect["count_new_only"] = True - effects.append(effect) - if plan.mode == "update": - target_fields.update(g.changed_columns) - # A new row a link row names (a new email address). - for p, ref in plan.added_refs.items(): - if p not in fields or found: - continue - effects.append( - _raw_effect( - b, - schema, - surfaces, - settings, - ref["table"], - {ref["column"]: _value_expr(p, ref["rule"])}, - 1, - {}, - f"the new {ref['table']} row holds {p}", + groups = [ + (p, g) + for p, g in plan.children.items() + if p in fields + or (not found and g.empty_when_null) + or (not found and plan.mode == "create" and set(g.co_owners) & fields) + ] + groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] + for p, g in groups: + name = f"rows:{g.table}" + sname = _alias(name) + if sname not in surfaces: + cols = _base_columns(b, g.table) + link = g.link + # The main record's values, seen from the child row. + for ip in identity_params: + loc = locations[ip] + up = Location( + (Hop("fk", anchor, link.column, link.ref_column, True),) + loc.hops, + loc.columns, + loc.rule, ) + expr = b.path("t", g.table, up) + if loc.rule in ("number", "date", "datetime", "minutes"): + expr = b.flag(expr, loc.rule, b.ph(ip)) + cols.append((f"main__{_alias(ip)}", expr)) + surfaces[sname] = Surface(sname, settings.views.get(g.table, g.table), cols) + named_tables.add(g.table) + surf = surfaces[sname] + items = range(variant.lists[p]) if g.list_param else [None] + for i in items: + m: dict[str, Any] = {} + for ip in identity_params: + loc = locations[ip] + if loc.rule in ("number", "date", "datetime", "minutes"): + m[f"main__{_alias(ip)}"] = {"literal": "1"} + else: + m[f"main__{_alias(ip)}"] = _value_expr(ip, loc.rule) + for k, loc in g.params.items(): + pname = k if g.list_param is None else f"{p}.{i}.{k}" + if g.list_param is None and pname not in fields: + # Given another owner but not this value: empty. + for c in loc.columns if not loc.hops else [loc.hops[0].column]: + alias = f"empty__{_alias(c)}" + if alias not in {a for a, _ in surf.columns}: + surf.columns.append((alias, b.empty("t", g.table, c))) + m[alias] = {"literal": "1"} + continue + key = _place(b, surf, g.table, pname, loc) + if loc.rule in ("number", "date", "datetime", "minutes"): + m[key] = {"literal": "1"} + else: + m[key] = _value_expr(pname, loc.rule) + for col, role in sorted(g.roles.items()): + if role.role == "link" or found: + continue + _pin(b, surf, g.table, col, role, m, settings.actor_params) + effect = { + "kind": "record_written", + "probe": f"surface={sname}|a row beside the main record holds {p}", + "match": m, + "expected_count": 1, + } + if not found: + # Found: the row is already there, and nothing changes. + effect["count_new_only"] = True + effects.append(effect) + if plan.mode == "update": + target_fields.update(g.changed_columns) + # A new row a link row names (a new email address). + for p, ref in plan.added_refs.items(): + if p not in fields or found: + continue + effects.append( + _raw_effect( + b, + schema, + surfaces, + settings, + ref["table"], + {ref["column"]: _value_expr(p, ref["rule"])}, + 1, + {}, + f"the new {ref['table']} row holds {p}", ) - for side in plan.sides: - if found or (side.unless is not None and side.unless in fields): - continue - m = {} - for col, role in sorted(side.roles.items()): - if role.role == "constant" and role.value is not None: - m[col] = {"literal": role.value} - elif role.role == "actor": - m[col] = {"param": role.detail["param"]} - effects.append( - _raw_effect( - b, - schema, - surfaces, - settings, - side.table, - m, - side.count, - side.roles, - f"{side.count} row(s) the app adds to {side.table}", - ) + ) + for side in plan.sides: + if found or (side.unless is not None and side.unless in fields): + continue + m = {} + for col, role in sorted(side.roles.items()): + if role.role == "constant" and role.value is not None: + m[col] = {"literal": role.value} + elif role.role == "actor": + m[col] = {"param": role.detail["param"]} + effects.append( + _raw_effect( + b, + schema, + surfaces, + settings, + side.table, + m, + side.count, + side.roles, + f"{side.count} row(s) the app adds to {side.table}", ) + ) verifier: dict[str, Any] = {"kind": "surfaces", "surfaces": {}} for s in surfaces.values(): @@ -2259,12 +2255,12 @@ def choose( def _exists(contract: Any, params: Mapping[str, Any]) -> bool: - """Does a record matching the found contract's main effect exist now?""" + """Does every effect of the found contract hold now (the record, and each + row beside it that holds a value)?""" from openadapt_flow.runtime.effects.effect import record_matches - from openadapt_flow.verify_run import _build_verifier - from openadapt_flow.runtime.effects.surfaces import surface_of + from openadapt_flow.verify_run import _build_verifier verifier = _build_verifier(contract.config, params) state = verifier.capture_pre_state() From 798289830b123d60f6cba1bc6f52ec26b311f96a Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:20:46 -0400 Subject: [PATCH 55/94] WIP: copied columns need two rows --- packages/flow/openadapt_flow/generate/demo_analysis.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index c3c3b9bfdb..2ae42df47f 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -665,7 +665,9 @@ def _copied( every row? (An invoice line's unit price is its product's price.)""" col = t.column(name) - if col is None or col.kind not in ("number", "text"): + if col is None or col.kind not in ("number", "text") or len(rows) < 2: + # One row can't tell a copy from a coincidence (a user id equal to a + # project id). return None candidates: Optional[set[tuple[str, str, str, str]]] = None for demo, r in rows: @@ -675,7 +677,7 @@ def _copied( idx = _Index(schema, demo.after) here: set[tuple[str, str, str, str]] = set() for other in t.columns: - if other.name == name or r.get(other.name) is None: + if other.name == name or other.name in t.primary_key or r.get(other.name) is None: continue for target, ref, _declared in _fk_targets( schema, idx, t, other.name, str(r.get(other.name)) From 10559df600c94f53bd0c8db1899a7a5fad577305 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:42:01 -0400 Subject: [PATCH 56/94] WIP: EspoCRM shapes: anchor scoring, tie-breaks, dispatch child rows, extra child places, views in subqueries --- .../generate_contracts/demo_espocrm.py | 8 +- .../results/redmine/QUESTIONS.md | 12 + .../results/redmine/answers.yaml | 1 + ...oject+subject+tracker.47c18f25.create.yaml | 674 +++++++++++ ...ject+project+spent_on.a2ffb74d.create.yaml | 578 +++++++++ ...ours+project+spent_on.c6f70a8e.create.yaml | 578 +++++++++ ...ject+project+spent_on.d1e5805e.create.yaml | 579 +++++++++ ...ours+project+spent_on.e07feee9.create.yaml | 579 +++++++++ ...roject+status+subject.4aa4d777.update.yaml | 590 +++++++++ .../results/redmine/manifest.yaml | 1052 +++++++++++++++++ .../results/redmine/report.json | 132 +++ .../openadapt_flow/generate/demo_analysis.py | 16 +- .../generate/verify_contract.py | 176 ++- 13 files changed, 4953 insertions(+), 22 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/report.json diff --git a/packages/flow/benchmark/generate_contracts/demo_espocrm.py b/packages/flow/benchmark/generate_contracts/demo_espocrm.py index 3b6c3ca9da..84feac8369 100644 --- a/packages/flow/benchmark/generate_contracts/demo_espocrm.py +++ b/packages/flow/benchmark/generate_contracts/demo_espocrm.py @@ -79,7 +79,9 @@ def api(method: str, path: str, payload: dict) -> None: cur.execute("SELECT COUNT(*) FROM contact WHERE BINARY first_name=%s AND BINARY last_name=%s AND deleted=0", (v["first_name"], v["last_name"])) # fmt: skip if cur.fetchone()[0]: - continue + # Already there: a create with values of our own. + v = {**v, "first_name": "Lena", "last_name": "Okafor", + "email": "lena.okafor@aurora-logistics.example"} body = {"firstName": v["first_name"], "lastName": v["last_name"], "accountId": account(v["account"], v["account_city"])} # fmt: skip if v.get("title"): @@ -96,7 +98,9 @@ def api(method: str, path: str, payload: dict) -> None: (v["name"],), ) if cur.fetchone()[0]: - continue + v = {**v, "name": "Aurora – Fleet Telematics Trial", "account": "Aurora Logistics", + "account_city": "Boston", "amount": 12500, "stage": "Proposal", + "close_date": "2027-02-15"} body = {"name": v["name"], "accountId": account(v["account"], v["account_city"]), "amount": v["amount"], "amountCurrency": v["currency"], "stage": v["stage"], "closeDate": v["close_date"]} # fmt: skip diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md new file mode 100644 index 0000000000..cef010825f --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md @@ -0,0 +1,12 @@ +# Questions for a person + +1. Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): custom_values (2 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change; on PostgreSQL they are not read at all): journal_details Columns that change on their own and are not compared: issues: lock_version, updated_on; user_preferences: others Columns of a new record that differ between demonstrations and are not checked: time_entries: tmonth, tweek, tyear Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. + - Answer key: `bookkeeping`. Proposed: `accept`. + - Answered: `"accept"` + +## Kinds + +- `create_issue` (create, main table `issues`): 26 contract(s). +- `update_issue` (update, main table `issues`): 63 contract(s). +- `log_time` (create, main table `time_entries`): 4 contract(s). +- `ensure_issue` (create, main table `issues`): 2 contract(s). diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml new file mode 100644 index 0000000000..402fc2163f --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml @@ -0,0 +1 @@ +bookkeeping: accept diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml new file mode 100644 index 0000000000..9ffc2e8b23 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml @@ -0,0 +1,674 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_issue, create, fields assignee, customer_ref, due_date, environment, priority, project, subject, tracker. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."firstname" + AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) FROM "users" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" + AS TEXT)) AS "p__assignee", CASE WHEN t."due_date" = CAST(%(v0)s AS date) THEN '1' ELSE '0' END + AS "p__due_date", (SELECT CAST(a1."name" AS TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS + TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CAST(a1."name" AS TEXT) FROM + "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT + CAST(a1."name" AS TEXT) FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" AS + TEXT)) AS "p__tracker", CASE WHEN t."category_id" IS NULL THEN '1' ELSE '0' END AS "pin__category_id", + CASE WHEN t."closed_on" IS NULL THEN '1' ELSE '0' END AS "pin__closed_on", CASE WHEN CAST(t."done_ratio" + AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__done_ratio", CASE WHEN t."estimated_hours" + IS NULL THEN '1' ELSE '0' END AS "pin__estimated_hours", CASE WHEN t."fixed_version_id" IS NULL + THEN '1' ELSE '0' END AS "pin__fixed_version_id", CASE WHEN CAST(CAST(t."is_private" AS integer) + AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__is_private", CASE WHEN CAST(t."lft" + AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__lft", CASE WHEN CAST(t."lock_version" + AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__lock_version", CASE WHEN t."parent_id" + IS NULL THEN '1' ELSE '0' END AS "pin__parent_id", CASE WHEN CAST(t."rgt" AS numeric) = CAST('2' + AS numeric) THEN '1' ELSE '0' END AS "pin__rgt", CASE WHEN t."start_date" IS NULL THEN '1' ELSE + '0' END AS "pin__start_date", CASE WHEN CAST(t."status_id" AS numeric) = CAST('1' AS numeric) + THEN '1' ELSE '0' END AS "pin__status_id", CASE WHEN t."description" IS NULL OR t."description" + = '' THEN '1' ELSE '0' END AS "empty__description" FROM "issues" t + sql_query_params: + v0: &id002 + param: due_date + rows_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CAST(a2."firstname" AS TEXT) || ' ' || CAST(a2."lastname" + AS TEXT) FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" AS TEXT)) FROM + "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", CASE WHEN (SELECT a1."due_date" + FROM "issues" a1 WHERE a1."id" = t."customized_id") = CAST(%(v0)s AS date) THEN '1' ELSE '0' END + AS "main__due_date", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "enumerations" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."priority_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") + AS "main__priority", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") + AS "main__project", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") + AS "main__subject", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "trackers" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."tracker_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") + AS "main__tracker", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('1' AS numeric) THEN + '1' ELSE '0' END AS "pin__custom_field_id", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('2' + AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id__47c66b" FROM "custom_values" t + sql_query_params: + v0: *id002 + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id" FROM "journals" t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_time_entries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__assignee: + param: assignee + p__due_date: + literal: '1' + p__priority: + param: priority + p__project: + param: project + subject: + param: subject + p__tracker: + param: tracker + author_id: + param: bot_user_id + pin__category_id: + literal: '1' + pin__closed_on: + literal: '1' + pin__done_ratio: + literal: '1' + pin__estimated_hours: + literal: '1' + pin__fixed_version_id: + literal: '1' + pin__is_private: + literal: '1' + pin__lft: + literal: '1' + pin__lock_version: + literal: '1' + pin__parent_id: + literal: '1' + pin__rgt: + literal: '1' + pin__start_date: + literal: '1' + pin__status_id: + literal: '1' + empty__description: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_custom_values|a row beside the main record holds customer_ref + match: + main__assignee: + param: assignee + main__due_date: + literal: '1' + main__priority: + param: priority + main__project: + param: project + main__subject: + param: subject + main__tracker: + param: tracker + value: + param: customer_ref + pin__custom_field_id: + literal: '1' + customized_type: + literal: Issue + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_custom_values|a row beside the main record holds environment + match: + main__assignee: + param: assignee + main__due_date: + literal: '1' + main__priority: + param: priority + main__project: + param: project + main__subject: + param: subject + main__tracker: + param: tracker + value: + param: environment + pin__custom_field_id__47c66b: + literal: '1' + customized_type: + literal: Issue + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: [] diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml new file mode 100644 index 0000000000..0de24b0cd5 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml @@ -0,0 +1,578 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, comments, hours, issue_subject, project, spent_on. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS + TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", + CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", + (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."issue_id" + AS TEXT)) AS "p__issue_subject", (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s + AS date) THEN '1' ELSE '0' END AS "p__spent_on" FROM "time_entries" t + sql_query_params: + v0: + param: hours + v1: + param: spent_on + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id" FROM "journals" t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__activity: + param: activity + comments: + param: comments + p__hours: + literal: '1' + p__issue_subject: + param: issue_subject + p__project: + param: project + p__spent_on: + literal: '1' + author_id: + param: bot_user_id + user_id: + param: bot_user_id + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: [] diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml new file mode 100644 index 0000000000..edbd5dd64f --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml @@ -0,0 +1,578 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, comments, hours, project, spent_on. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS + TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", + CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", + (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" + AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END + AS "p__spent_on", CASE WHEN t."issue_id" IS NULL THEN '1' ELSE '0' END AS "empty__issue_id" FROM + "time_entries" t + sql_query_params: + v0: + param: hours + v1: + param: spent_on + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id" FROM "journals" t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__activity: + param: activity + comments: + param: comments + p__hours: + literal: '1' + p__project: + param: project + p__spent_on: + literal: '1' + author_id: + param: bot_user_id + user_id: + param: bot_user_id + empty__issue_id: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: [] diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml new file mode 100644 index 0000000000..e1d5b82da8 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml @@ -0,0 +1,579 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, hours, issue_subject, project, spent_on. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS + TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", + CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", + (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."issue_id" + AS TEXT)) AS "p__issue_subject", (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s + AS date) THEN '1' ELSE '0' END AS "p__spent_on", CASE WHEN t."comments" IS NULL OR t."comments" + = '' THEN '1' ELSE '0' END AS "empty__comments" FROM "time_entries" t + sql_query_params: + v0: + param: hours + v1: + param: spent_on + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id" FROM "journals" t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__activity: + param: activity + p__hours: + literal: '1' + p__issue_subject: + param: issue_subject + p__project: + param: project + p__spent_on: + literal: '1' + author_id: + param: bot_user_id + user_id: + param: bot_user_id + empty__comments: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: [] diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml new file mode 100644 index 0000000000..80afe1cb97 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml @@ -0,0 +1,579 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, hours, project, spent_on. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS + TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", + CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", + (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" + AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END + AS "p__spent_on", CASE WHEN t."comments" IS NULL OR t."comments" = '' THEN '1' ELSE '0' END AS + "empty__comments", CASE WHEN t."issue_id" IS NULL THEN '1' ELSE '0' END AS "empty__issue_id" FROM + "time_entries" t + sql_query_params: + v0: + param: hours + v1: + param: spent_on + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id" FROM "journals" t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__activity: + param: activity + p__hours: + literal: '1' + p__project: + param: project + p__spent_on: + literal: '1' + author_id: + param: bot_user_id + user_id: + param: bot_user_id + empty__comments: + literal: '1' + empty__issue_id: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: [] diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml new file mode 100644 index 0000000000..3c41dbe83f --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml @@ -0,0 +1,590 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_issue, update, fields notes, project, status, subject. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-redmine +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: psycopg2 + sql_connect_args: &id001 + dbname: redmine + host: 127.0.0.1 + port: 9606 + user: oa_reader + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."name" AS + TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", + (SELECT CAST(a1."name" AS TEXT) FROM "issue_statuses" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."status_id" + AS TEXT)) AS "p__status" FROM "issues" t + rows_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE + CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__project", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "issue_statuses" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."status_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__status", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__subject", CASE WHEN CAST(CAST(t."private_notes" AS integer) AS numeric) = CAST('0' AS + numeric) THEN '1' ELSE '0' END AS "pin__private_notes", CASE WHEN t."updated_by_id" IS NULL THEN + '1' ELSE '0' END AS "pin__updated_by_id", CASE WHEN CAST(t."user_id" AS numeric) = CAST('5' AS + numeric) THEN '1' ELSE '0' END AS "pin__user_id" FROM "journals" t + raw_ar_internal_metadata: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" + AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) + AS "updated_at" FROM "ar_internal_metadata" t + raw_attachments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", + CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", + CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", + CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" + AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) + AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) + AS "disk_directory" FROM "attachments" t + raw_auth_sources: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" + AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" + AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS + TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" + AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" + AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", + CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t + raw_boards: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) + AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) + AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" + AS TEXT) AS "parent_id" FROM "boards" t + raw_changes: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", + CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS + TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS + TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t + raw_changeset_parents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) + AS "parent_id" FROM "changeset_parents" t + raw_changesets: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", + CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" + AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS + TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "changesets" t + raw_changesets_issues: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS + "issue_id" FROM "changesets_issues" t + raw_comments: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", + CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", + CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on" FROM "comments" t + raw_custom_field_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) + AS "position" FROM "custom_field_enumerations" t + raw_custom_fields: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" + AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" + AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) + AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS + "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", + CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" + AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) + AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", + CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t + raw_custom_fields_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS + TEXT) AS "project_id" FROM "custom_fields_projects" t + raw_custom_fields_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) + AS "role_id" FROM "custom_fields_roles" t + raw_custom_fields_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS + TEXT) AS "tracker_id" FROM "custom_fields_trackers" t + raw_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t + raw_documents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" + AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS + "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t + raw_email_addresses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" + AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) + AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" + FROM "email_addresses" t + raw_enabled_modules: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name" FROM "enabled_modules" t + raw_enumerations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS + "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" + t + raw_groups_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" + FROM "groups_users" t + raw_import_items: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" + AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", + CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t + raw_imports: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS + "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", + CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM + "imports" t + raw_issue_categories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" + t + raw_issue_relations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", + CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", + CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t + raw_issue_statuses: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" + AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" + AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" + t + raw_member_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" + t + raw_members: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" + AS TEXT) AS "mail_notification" FROM "members" t + raw_messages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" + AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS + "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", + CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" + AS TEXT) AS "sticky" FROM "messages" t + raw_news: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS + "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t + raw_oauth_access_grants: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" + AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" + AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t + raw_oauth_access_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", + CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" + AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" + AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) + AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" + t + raw_oauth_applications: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS + TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", + CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" + t + raw_projects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" + AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) + AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" + AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" + AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" + AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" + FROM "projects" t + raw_projects_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS + "tracker_id" FROM "projects_trackers" t + raw_queries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", + CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" + AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) + AS "description" FROM "queries" t + raw_queries_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" + FROM "queries_roles" t + raw_reactions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", + CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" + AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t + raw_repositories: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" + AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", + CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" + AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" + AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS + TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t + raw_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) + AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) + AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" + AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", + CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS + "default_time_entry_activity_id" FROM "roles" t + raw_roles_managed_roles: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" + FROM "roles_managed_roles" t + raw_schema_migrations: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM + "schema_migrations" t + raw_settings: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" + AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_time_entries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t + raw_tokens: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" + AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t + raw_trackers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" + AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" + AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" + AS TEXT) AS "description" FROM "trackers" t + raw_user_preferences: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" + AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) + AS "time_zone" FROM "user_preferences" t + raw_users: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" + AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS + TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", + CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", + CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" + AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" + AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", + CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", + CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" + AS TEXT) AS "twofa_required" FROM "users" t + raw_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" + AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS + TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS + TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" + AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t + raw_watchers: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", + CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM + "watchers" t + raw_wiki_content_versions: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", + CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", + 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS + "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" + FROM "wiki_content_versions" t + raw_wiki_contents: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" + AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", + CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" + t + raw_wiki_pages: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) + AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t + raw_wiki_redirects: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" + AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS + TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" + t + raw_wikis: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" + AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t + raw_workflows: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" + AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" + AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", + CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" + AS TEXT) AS "rule" FROM "workflows" t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__project: + param: project + p__status: + param: status + subject: + param: subject + expected_count: 1 +- kind: record_written + probe: surface=rows_journals|a row beside the main record holds notes + match: + main__project: + param: project + main__status: + param: status + main__subject: + param: subject + notes: + param: notes + journalized_type: + literal: Issue + pin__private_notes: + literal: '1' + pin__updated_by_id: + literal: '1' + pin__user_id: + literal: '1' + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - lock_version + - others + - updated_on + target_fields: + - p__status + - status_id diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml new file mode 100644 index 0000000000..aca034aa1e --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml @@ -0,0 +1,1052 @@ +schema_version: openadapt.contract-manifest/v1 +dialect: postgresql +database: redmine +actor_params: +- bot_user_id +questions_open: [] +kinds: + create_issue: + mode: create + main_table: issues + parameters: + - project + - tracker + - subject + - priority + - assignee + - due_date + - description + - customer_ref + - environment + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - assignee + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+due_date+priority+project+subject+tracker.cb8524d8.create.yaml + found: contracts/create_issue/assignee+due_date+priority+project+subject+tracker.cb8524d8.found.yaml + - fields: + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.create.yaml + found: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.found.yaml + - fields: + - assignee + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.create.yaml + found: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.found.yaml + - fields: + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/priority+project+subject+tracker.a0dc8790.create.yaml + found: contracts/create_issue/priority+project+subject+tracker.a0dc8790.found.yaml + - fields: + - assignee + - customer_ref + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.create.yaml + found: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.found.yaml + - fields: + - customer_ref + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+environment+priority+project+subject+tracker.e94e6033.create.yaml + found: contracts/create_issue/customer_ref+environment+priority+project+subject+tracker.e94e6033.found.yaml + - fields: + - assignee + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+environment+priority+project+subject+tracker.b1832d09.create.yaml + found: contracts/create_issue/assignee+environment+priority+project+subject+tracker.b1832d09.found.yaml + - fields: + - assignee + - customer_ref + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+priority+project+subject+tracker.88f7523d.create.yaml + found: contracts/create_issue/assignee+customer_ref+priority+project+subject+tracker.88f7523d.found.yaml + - fields: + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/environment+priority+project+subject+tracker.99d89352.create.yaml + found: contracts/create_issue/environment+priority+project+subject+tracker.99d89352.found.yaml + - fields: + - customer_ref + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+priority+project+subject+tracker.322376d5.create.yaml + found: contracts/create_issue/customer_ref+priority+project+subject+tracker.322376d5.found.yaml + - fields: + - assignee + - customer_ref + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+due_date+priority+project+subject+tracker.f7bf10f1.create.yaml + found: contracts/create_issue/assignee+customer_ref+due_date+priority+project+subject+tracker.f7bf10f1.found.yaml + - fields: + - assignee + - description + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+description+due_date+priority+project+subject+tracker.3045a6ca.create.yaml + found: contracts/create_issue/assignee+description+due_date+priority+project+subject+tracker.3045a6ca.found.yaml + - fields: + - assignee + - due_date + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+due_date+environment+priority+project+subject+tracker.9f2faa3a.create.yaml + found: contracts/create_issue/assignee+due_date+environment+priority+project+subject+tracker.9f2faa3a.found.yaml + - fields: + - customer_ref + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.create.yaml + found: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.found.yaml + - fields: + - description + - due_date + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.create.yaml + found: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.found.yaml + - fields: + - due_date + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.create.yaml + found: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.found.yaml + - fields: + - assignee + - description + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.create.yaml + found: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.found.yaml + - fields: + - description + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.create.yaml + found: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.found.yaml + - fields: + - assignee + - customer_ref + - description + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml + found: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml + - fields: + - assignee + - customer_ref + - due_date + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml + found: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.found.yaml + - fields: + - customer_ref + - description + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+description+environment+priority+project+subject+tracker.8bd3a0b3.create.yaml + found: contracts/create_issue/customer_ref+description+environment+priority+project+subject+tracker.8bd3a0b3.found.yaml + - fields: + - customer_ref + - due_date + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+due_date+environment+priority+project+subject+tracker.018e5a55.create.yaml + found: contracts/create_issue/customer_ref+due_date+environment+priority+project+subject+tracker.018e5a55.found.yaml + - fields: + - assignee + - description + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+description+environment+priority+project+subject+tracker.1563d2ca.create.yaml + found: contracts/create_issue/assignee+description+environment+priority+project+subject+tracker.1563d2ca.found.yaml + - fields: + - assignee + - customer_ref + - description + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/assignee+customer_ref+description+priority+project+subject+tracker.4d3aeb69.create.yaml + found: contracts/create_issue/assignee+customer_ref+description+priority+project+subject+tracker.4d3aeb69.found.yaml + - fields: + - description + - environment + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/description+environment+priority+project+subject+tracker.4203fe30.create.yaml + found: contracts/create_issue/description+environment+priority+project+subject+tracker.4203fe30.found.yaml + - fields: + - customer_ref + - description + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/customer_ref+description+priority+project+subject+tracker.c483a119.create.yaml + found: contracts/create_issue/customer_ref+description+priority+project+subject+tracker.c483a119.found.yaml + update_issue: + mode: update + main_table: issues + parameters: + - project + - subject + - status + - assignee + - priority + - due_date + - done_ratio + - notes + identity: + - project + - subject + lists: [] + dispatch: null + variants: + - fields: + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/project+status+subject.f5cf2619.update.yaml + found: contracts/update_issue/project+status+subject.f5cf2619.found.yaml + - fields: + - assignee + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+project+subject.4e643112.update.yaml + found: contracts/update_issue/assignee+project+subject.4e643112.found.yaml + - fields: + - due_date + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+project+subject.4f8546da.update.yaml + found: contracts/update_issue/due_date+project+subject.4f8546da.found.yaml + - fields: + - done_ratio + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+project+subject.5f3c3fa0.update.yaml + found: contracts/update_issue/done_ratio+project+subject.5f3c3fa0.found.yaml + - fields: + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/notes+project+subject.05b2ea74.update.yaml + found: contracts/update_issue/notes+project+subject.05b2ea74.found.yaml + - fields: + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/priority+project+subject.a57fbe91.update.yaml + found: contracts/update_issue/priority+project+subject.a57fbe91.found.yaml + - fields: + - assignee + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+project+status+subject.a05db09f.update.yaml + found: contracts/update_issue/assignee+project+status+subject.a05db09f.found.yaml + - fields: + - due_date + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+project+status+subject.0af96bde.update.yaml + found: contracts/update_issue/due_date+project+status+subject.0af96bde.found.yaml + - fields: + - done_ratio + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+project+status+subject.474ffed4.update.yaml + found: contracts/update_issue/done_ratio+project+status+subject.474ffed4.found.yaml + - fields: + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml + found: contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml + - fields: + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/priority+project+status+subject.bbe05441.update.yaml + found: contracts/update_issue/priority+project+status+subject.bbe05441.found.yaml + - fields: + - assignee + - due_date + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+project+subject.159f21e0.update.yaml + found: contracts/update_issue/assignee+due_date+project+subject.159f21e0.found.yaml + - fields: + - assignee + - done_ratio + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+project+subject.5df67a01.update.yaml + found: contracts/update_issue/assignee+done_ratio+project+subject.5df67a01.found.yaml + - fields: + - assignee + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+notes+project+subject.bbff0247.update.yaml + found: contracts/update_issue/assignee+notes+project+subject.bbff0247.found.yaml + - fields: + - assignee + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+priority+project+subject.b5e14879.update.yaml + found: contracts/update_issue/assignee+priority+project+subject.b5e14879.found.yaml + - fields: + - done_ratio + - due_date + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+project+subject.9d543b40.update.yaml + found: contracts/update_issue/done_ratio+due_date+project+subject.9d543b40.found.yaml + - fields: + - due_date + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+notes+project+subject.6f69bb62.update.yaml + found: contracts/update_issue/due_date+notes+project+subject.6f69bb62.found.yaml + - fields: + - due_date + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+priority+project+subject.119bebbc.update.yaml + found: contracts/update_issue/due_date+priority+project+subject.119bebbc.found.yaml + - fields: + - done_ratio + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+notes+project+subject.42cc7615.update.yaml + found: contracts/update_issue/done_ratio+notes+project+subject.42cc7615.found.yaml + - fields: + - done_ratio + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+priority+project+subject.3f024f0d.update.yaml + found: contracts/update_issue/done_ratio+priority+project+subject.3f024f0d.found.yaml + - fields: + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/notes+priority+project+subject.8682b0ac.update.yaml + found: contracts/update_issue/notes+priority+project+subject.8682b0ac.found.yaml + - fields: + - assignee + - due_date + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+project+status+subject.c3a69cb4.update.yaml + found: contracts/update_issue/assignee+due_date+project+status+subject.c3a69cb4.found.yaml + - fields: + - assignee + - done_ratio + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+project+status+subject.54276404.update.yaml + found: contracts/update_issue/assignee+done_ratio+project+status+subject.54276404.found.yaml + - fields: + - assignee + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+notes+project+status+subject.c7d221b1.update.yaml + found: contracts/update_issue/assignee+notes+project+status+subject.c7d221b1.found.yaml + - fields: + - assignee + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+priority+project+status+subject.bc85aafd.update.yaml + found: contracts/update_issue/assignee+priority+project+status+subject.bc85aafd.found.yaml + - fields: + - done_ratio + - due_date + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+project+status+subject.46328550.update.yaml + found: contracts/update_issue/done_ratio+due_date+project+status+subject.46328550.found.yaml + - fields: + - due_date + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+notes+project+status+subject.73fff321.update.yaml + found: contracts/update_issue/due_date+notes+project+status+subject.73fff321.found.yaml + - fields: + - due_date + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+priority+project+status+subject.6461d8ca.update.yaml + found: contracts/update_issue/due_date+priority+project+status+subject.6461d8ca.found.yaml + - fields: + - done_ratio + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+notes+project+status+subject.aec2705d.update.yaml + found: contracts/update_issue/done_ratio+notes+project+status+subject.aec2705d.found.yaml + - fields: + - done_ratio + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+priority+project+status+subject.5b1b1f40.update.yaml + found: contracts/update_issue/done_ratio+priority+project+status+subject.5b1b1f40.found.yaml + - fields: + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/notes+priority+project+status+subject.a5a6c598.update.yaml + found: contracts/update_issue/notes+priority+project+status+subject.a5a6c598.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+project+subject.e06f0a37.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+project+subject.e06f0a37.found.yaml + - fields: + - assignee + - due_date + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+notes+project+subject.177404ca.update.yaml + found: contracts/update_issue/assignee+due_date+notes+project+subject.177404ca.found.yaml + - fields: + - assignee + - due_date + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+priority+project+subject.bed7fd8a.update.yaml + found: contracts/update_issue/assignee+due_date+priority+project+subject.bed7fd8a.found.yaml + - fields: + - assignee + - done_ratio + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+notes+project+subject.7054e59a.update.yaml + found: contracts/update_issue/assignee+done_ratio+notes+project+subject.7054e59a.found.yaml + - fields: + - assignee + - done_ratio + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+priority+project+subject.887e7284.update.yaml + found: contracts/update_issue/assignee+done_ratio+priority+project+subject.887e7284.found.yaml + - fields: + - assignee + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+notes+priority+project+subject.c8e8610c.update.yaml + found: contracts/update_issue/assignee+notes+priority+project+subject.c8e8610c.found.yaml + - fields: + - done_ratio + - due_date + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+notes+project+subject.036ba8cb.update.yaml + found: contracts/update_issue/done_ratio+due_date+notes+project+subject.036ba8cb.found.yaml + - fields: + - done_ratio + - due_date + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+priority+project+subject.530b95ee.update.yaml + found: contracts/update_issue/done_ratio+due_date+priority+project+subject.530b95ee.found.yaml + - fields: + - due_date + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+notes+priority+project+subject.185b2051.update.yaml + found: contracts/update_issue/due_date+notes+priority+project+subject.185b2051.found.yaml + - fields: + - done_ratio + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+notes+priority+project+subject.3b999ec2.update.yaml + found: contracts/update_issue/done_ratio+notes+priority+project+subject.3b999ec2.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+project+status+subject.80ad363e.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+project+status+subject.80ad363e.found.yaml + - fields: + - assignee + - due_date + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+notes+project+status+subject.07561ad0.update.yaml + found: contracts/update_issue/assignee+due_date+notes+project+status+subject.07561ad0.found.yaml + - fields: + - assignee + - due_date + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+priority+project+status+subject.9b95d736.update.yaml + found: contracts/update_issue/assignee+due_date+priority+project+status+subject.9b95d736.found.yaml + - fields: + - assignee + - done_ratio + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+notes+project+status+subject.b2c7c267.update.yaml + found: contracts/update_issue/assignee+done_ratio+notes+project+status+subject.b2c7c267.found.yaml + - fields: + - assignee + - done_ratio + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+priority+project+status+subject.276a5a30.update.yaml + found: contracts/update_issue/assignee+done_ratio+priority+project+status+subject.276a5a30.found.yaml + - fields: + - assignee + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+notes+priority+project+status+subject.d1cc724b.update.yaml + found: contracts/update_issue/assignee+notes+priority+project+status+subject.d1cc724b.found.yaml + - fields: + - done_ratio + - due_date + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+notes+project+status+subject.f2171a53.update.yaml + found: contracts/update_issue/done_ratio+due_date+notes+project+status+subject.f2171a53.found.yaml + - fields: + - done_ratio + - due_date + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+priority+project+status+subject.14f98761.update.yaml + found: contracts/update_issue/done_ratio+due_date+priority+project+status+subject.14f98761.found.yaml + - fields: + - due_date + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/due_date+notes+priority+project+status+subject.29f05bb8.update.yaml + found: contracts/update_issue/due_date+notes+priority+project+status+subject.29f05bb8.found.yaml + - fields: + - done_ratio + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+notes+priority+project+status+subject.eec8202f.update.yaml + found: contracts/update_issue/done_ratio+notes+priority+project+status+subject.eec8202f.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - notes + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+notes+project+subject.7ff66a52.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+notes+project+subject.7ff66a52.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+priority+project+subject.6eb0e43c.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+priority+project+subject.6eb0e43c.found.yaml + - fields: + - assignee + - due_date + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+notes+priority+project+subject.88ebb1f9.update.yaml + found: contracts/update_issue/assignee+due_date+notes+priority+project+subject.88ebb1f9.found.yaml + - fields: + - assignee + - done_ratio + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+notes+priority+project+subject.4153e619.update.yaml + found: contracts/update_issue/assignee+done_ratio+notes+priority+project+subject.4153e619.found.yaml + - fields: + - done_ratio + - due_date + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+notes+priority+project+subject.8da31395.update.yaml + found: contracts/update_issue/done_ratio+due_date+notes+priority+project+subject.8da31395.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - notes + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+notes+project+status+subject.02a8e039.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+notes+project+status+subject.02a8e039.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+priority+project+status+subject.113ce920.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+priority+project+status+subject.113ce920.found.yaml + - fields: + - assignee + - due_date + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+due_date+notes+priority+project+status+subject.6ea08e8b.update.yaml + found: contracts/update_issue/assignee+due_date+notes+priority+project+status+subject.6ea08e8b.found.yaml + - fields: + - assignee + - done_ratio + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+notes+priority+project+status+subject.31317326.update.yaml + found: contracts/update_issue/assignee+done_ratio+notes+priority+project+status+subject.31317326.found.yaml + - fields: + - done_ratio + - due_date + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/done_ratio+due_date+notes+priority+project+status+subject.3cf03089.update.yaml + found: contracts/update_issue/done_ratio+due_date+notes+priority+project+status+subject.3cf03089.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - notes + - priority + - project + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+subject.514a3f86.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+subject.514a3f86.found.yaml + - fields: + - assignee + - done_ratio + - due_date + - notes + - priority + - project + - status + - subject + lists: {} + dispatch_value: null + update: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+status+subject.2d1635fd.update.yaml + found: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+status+subject.2d1635fd.found.yaml + log_time: + mode: create + main_table: time_entries + parameters: + - project + - issue_subject + - hours + - spent_on + - activity + - comments + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - activity + - comments + - hours + - issue_subject + - project + - spent_on + lists: {} + dispatch_value: null + create: contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml + found: contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml + - fields: + - activity + - hours + - issue_subject + - project + - spent_on + lists: {} + dispatch_value: null + create: contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml + found: contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.found.yaml + - fields: + - activity + - comments + - hours + - project + - spent_on + lists: {} + dispatch_value: null + create: contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml + found: contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.found.yaml + - fields: + - activity + - hours + - project + - spent_on + lists: {} + dispatch_value: null + create: contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml + found: contracts/log_time/activity+hours+project+spent_on.e07feee9.found.yaml + ensure_issue: + mode: create + main_table: issues + parameters: + - project + - tracker + - subject + - priority + - assignee + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - assignee + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/ensure_issue/assignee+priority+project+subject+tracker.d16f0df9.create.yaml + found: contracts/ensure_issue/assignee+priority+project+subject+tracker.d16f0df9.found.yaml + - fields: + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/ensure_issue/priority+project+subject+tracker.a0dc8790.create.yaml + found: contracts/ensure_issue/priority+project+subject+tracker.a0dc8790.found.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/report.json b/packages/flow/benchmark/generate_contracts/results/redmine/report.json new file mode 100644 index 0000000000..76309b9659 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/redmine/report.json @@ -0,0 +1,132 @@ +{ + "applied": [], + "dialect": "postgresql", + "tables": 58, + "questions": [ + { + "number": 1, + "key": "bookkeeping", + "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): custom_values (2 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change; on PostgreSQL they are not read at all): journal_details Columns that change on their own and are not compared: issues: lock_version, updated_on; user_preferences: others Columns of a new record that differ between demonstrations and are not checked: time_entries: tmonth, tweek, tyear Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", + "proposal": "accept", + "answered": true + } + ], + "kinds": { + "create_issue": { + "mode": "create", + "main_table": "issues", + "locations": { + "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", + "customer_ref": "custom_values (whose customized_id is the issues key).value (exact)", + "description": "issues.description (exact)", + "due_date": "issues.due_date (date)", + "environment": "custom_values (whose customized_id is the issues key).value (exact)", + "priority": "issues.priority_id -> enumerations.name (exact)", + "project": "issues.project_id -> projects.name (exact)", + "subject": "issues.subject (exact)", + "tracker": "issues.tracker_id -> trackers.name (exact)" + }, + "placed_by_name": [ + "description" + ], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": { + "customer_ref": "custom_values", + "environment": "custom_values" + }, + "lists": {}, + "side_rows": [], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 26, + "rejected": [] + }, + "update_issue": { + "mode": "update", + "main_table": "issues", + "locations": { + "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", + "done_ratio": "issues.done_ratio (number)", + "due_date": "issues.due_date (date)", + "notes": "journals (whose journalized_id is the issues key).notes (exact)", + "priority": "issues.priority_id -> enumerations.name (exact)", + "project": "issues.project_id -> projects.name (exact)", + "status": "issues.status_id -> issue_statuses.name (exact)", + "subject": "issues.subject (exact)" + }, + "placed_by_name": [ + "done_ratio" + ], + "placed_from_another_kind": [ + "due_date", + "priority" + ], + "demonstrations": 2, + "children": { + "notes": "journals" + }, + "lists": {}, + "side_rows": [], + "logs": [ + "journal_details" + ], + "refused": [], + "unresolved": {}, + "emitted": 63, + "rejected": [] + }, + "log_time": { + "mode": "create", + "main_table": "time_entries", + "locations": { + "activity": "time_entries.activity_id -> enumerations.name (exact)", + "comments": "time_entries.comments (exact)", + "hours": "time_entries.hours (number)", + "issue_subject": "time_entries.issue_id -> issues.subject (exact)", + "project": "time_entries.project_id -> projects.name (exact)", + "spent_on": "time_entries.spent_on (date)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": {}, + "lists": {}, + "side_rows": [], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 4, + "rejected": [] + }, + "ensure_issue": { + "mode": "create", + "main_table": "issues", + "locations": { + "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", + "priority": "issues.priority_id -> enumerations.name (exact)", + "project": "issues.project_id -> projects.name (exact)", + "subject": "issues.subject (exact)", + "tracker": "issues.tracker_id -> trackers.name (exact)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": {}, + "lists": {}, + "side_rows": [ + { + "table": "custom_values", + "count": 2, + "unless": null + } + ], + "logs": [], + "refused": [], + "unresolved": {}, + "emitted": 2, + "rejected": [] + } + } +} \ No newline at end of file diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 2ae42df47f..7eee2fcf2f 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -485,6 +485,16 @@ def pick_anchor( ) else: hit = locate(schema, demo, ch, v, max_hops=1) + # Only the row's own columns, and keys named after the table they + # point to: a row the write added beside the main record (an + # issue's journal) reaches the record's values only through a + # link back, and a key equal to some row's id by chance proves + # nothing. + hit = [ + loc + for loc in hit + if all(h.kind == "fk" and _hop_cost(h) <= 1 for h in loc.hops) + ] score += 1 if hit else 0 direct += 1 if any(not loc.hops for loc in hit) else 0 if (score, direct) > (best_score, best_direct): @@ -677,7 +687,11 @@ def _copied( idx = _Index(schema, demo.after) here: set[tuple[str, str, str, str]] = set() for other in t.columns: - if other.name == name or other.name in t.primary_key or r.get(other.name) is None: + if ( + other.name == name + or other.name in t.primary_key + or r.get(other.name) is None + ): continue for target, ref, _declared in _fk_targets( schema, idx, t, other.name, str(r.get(other.name)) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 5e546457f9..90240c1cea 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -195,6 +195,9 @@ class ChildGroup: #: The app adds this row with an empty value when the task leaves the #: parameter out (Redmine's custom field values): expected then too. empty_when_null: bool = False + #: The parameter whose value the rows hold (``children`` may hold a + #: second group for it: a call's lead is its parent and an attendee). + param: str = "" @dataclass @@ -234,10 +237,17 @@ class KindPlan: unresolved: dict[str, list[Location]] = field(default_factory=dict) refused: list[str] = field(default_factory=list) removed: dict[str, int] = field(default_factory=dict) + #: dispatch value -> group name -> rows that hold the value param. + dispatch_children: dict[str, dict[str, "ChildGroup"]] = field(default_factory=dict) + #: unresolved key -> (parameter, dispatch value or None). + unresolved_meta: dict[str, tuple[str, Optional[str]]] = field(default_factory=dict) #: Parameters placed by name, not by a demonstration. by_name: list[str] = field(default_factory=list) #: Parameters placed where another kind's demonstrations found them. shared: list[str] = field(default_factory=list) + #: More places a value is in: rows the write added beside the record + #: (a call's lead is its parent and also an attendee row). + extra: dict[str, list[Location]] = field(default_factory=dict) def _scalars(values: Mapping[str, Any]) -> dict[str, Any]: @@ -340,7 +350,7 @@ def analyze( for p in scalars: if p == plan.dispatch: continue - if plan.dispatch is not None: + if plan.dispatch is not None and "identity" not in roles_of[p]: by_value: dict[str, list[list[Location]]] = {} for d, locs in zip( [d for d in plan.demos if d.values.get(p) is not None], found[p] @@ -349,16 +359,31 @@ def analyze( if len(by_value) > 1: ok = True for dv, per in by_value.items(): - loc, tied = best(_intersect(per)) + loc, tied = _pick(p, _intersect(per)) if loc is None: - plan.unresolved[f"{p} (when {plan.dispatch} is {dv})"] = tied + key = f"{p} (when {plan.dispatch} is {dv})" + plan.unresolved[key] = tied + plan.unresolved_meta[key] = (p, dv) ok = False else: plan.dispatch_locations.setdefault(dv, {})[p] = loc if ok: plan.changes.append(p) continue - loc, tied = best(_intersect(found[p])) + all_found = _intersect(found[p]) + loc, tied = _pick(p, all_found) + if loc is not None: + plan.extra[p] = [ + x + for x in all_found + if x.hops + and x.hops[0].kind == "child" + and x != loc + # in another table than the main place, through keys named + # after their tables (a call's lead attendee row) + and (not loc.hops or x.hops[0].table != loc.hops[0].table) + and all(_hop_cost(h) <= 1 for h in x.hops[1:]) + ] if loc is None: if not found[p]: named = _by_name(schema, plan, p) @@ -369,6 +394,7 @@ def analyze( plan.changes.append(p) continue # never given: no contract uses it plan.unresolved[p] = tied + plan.unresolved_meta[p] = (p, None) continue plan.locations[p] = loc if plan.mode == "update": @@ -453,6 +479,64 @@ def _share_locations( break +def _pick( + param: str, found: Sequence[Location] +) -> tuple[Optional[Location], list[Location]]: + """The best location; among tied ones, the one whose column is named + like the parameter (``stage``, not ``last_stage``), if exactly one is.""" + + loc, tied = best(found) + if loc is not None or not tied: + return loc, tied + name = re.sub(r"^(new|set)_", "", param) + named = [t for t in tied if t.columns and t.columns[-1] == name] + if len(named) == 1: + return named[0], [] + return None, tied + + +def _resolve(plan: KindPlan, key: str, loc: Location) -> None: + """Place an unresolved value (answered, or settled by a tie-break).""" + + param, dv = plan.unresolved_meta.get(key, (key, None)) + plan.unresolved.pop(key, None) + if dv is not None: + plan.dispatch_locations.setdefault(dv, {})[param] = loc + if param not in plan.changes: + plan.changes.append(param) + return + plan.locations[param] = loc + if plan.mode == "update" and param not in plan.identity + plan.changes: + plan.changes.append(param) + + +def _tail(plan: KindPlan, loc: Location) -> tuple[str, tuple[str, ...]]: + return (loc.hops[-1].table if loc.hops else plan.anchor, loc.columns) + + +def _break_ties_across(plans: Sequence[KindPlan]) -> None: + """A value tied between columns of one table (an email's ``name`` and + ``lower``) goes where another kind's demonstrations put the same kind + of value, if exactly one tied place is one of those.""" + + for plan in plans: + for key, tied in list(plan.unresolved.items()): + if not tied: + continue + seen = { + _tail(o, loc) + for o in plans + if o is not plan + for loc in [ + *o.locations.values(), + *[x for d in o.dispatch_locations.values() for x in d.values()], + ] + } + hits = [t for t in tied if _tail(plan, t) in seen] + if len(hits) == 1: + _resolve(plan, key, hits[0]) + + def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: """An update parameter no demonstration gives (``new_zip``), placed at the main table's column of the same name after a leading ``new_`` or @@ -477,17 +561,33 @@ def _by_name(schema: Schema, plan: KindPlan, param: str) -> Optional[Location]: return Location((), (name,), rule) -def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: +def _children( + schema: Schema, plan: KindPlan, lists: Sequence[str], only_missing: bool = False +) -> None: """Group child-row locations, and find each list parameter's rows.""" - for p, loc in list(plan.locations.items()): + todo: list[tuple[str, str, Location, Optional[str]]] = [] + for p, loc in plan.locations.items(): + todo.append((p, p, loc, None)) + for i, x in enumerate(plan.extra.get(p, [])): + todo.append((f"{p}#{i + 2}", p, x, None)) + for dv, locs in plan.dispatch_locations.items(): + for p, loc in locs.items(): + todo.append((f"{p}@{dv}", p, loc, dv)) + for gname, p, loc, dv in todo: if not loc.hops or loc.hops[0].kind != "child": continue + if only_missing and ( + gname in plan.children or gname in plan.dispatch_children.get(dv or "", {}) + ): + continue rel = Location(loc.hops[1:], loc.columns, loc.rule) rows = [] for d, a in zip(plan.demos, plan.anchors): if d.values.get(p) is None: continue + if dv is not None and str(d.values.get(plan.dispatch)) != dv: + continue for ch in d.diff.of(loc.hops[0].table, "added"): if ch.after is None: continue @@ -522,14 +622,19 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: ): co.append(q) explained |= {h[0].hops[0].column for h in hits} - plan.children[p] = ChildGroup( + group = ChildGroup( loc.hops[0].table, loc.hops[0], {p: rel}, column_roles(schema, loc.hops[0].table, rows, explained, {}), 1, co_owners=co, + param=p, ) + if dv is None: + plan.children[gname] = group + else: + plan.dispatch_children.setdefault(dv, {})[gname] = group for p in lists: per_key: dict[str, list[list[Location]]] = {} table = link = None @@ -603,10 +708,26 @@ def _children(schema: Schema, plan: KindPlan, lists: Sequence[str]) -> None: ) +def _all_groups(plan: KindPlan, dv: Optional[str] = None) -> list[ChildGroup]: + """Every child group; with ``dv``, the plain ones and that dispatch + value's own.""" + + out = list(plan.children.values()) + for value, groups in plan.dispatch_children.items(): + if dv is None or value == dv: + out.extend(groups.values()) + return out + + +def _demo_groups(plan: KindPlan, d: Demo) -> list[ChildGroup]: + dv = str(d.values.get(plan.dispatch)) if plan.dispatch else None + return _all_groups(plan, dv) if dv is not None else list(plan.children.values()) + + def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> None: """Classify every other row the demonstrations touched.""" - child_tables = {g.table: p for p, g in plan.children.items()} + child_tables = {g.table: g.param for g in _all_groups(plan)} list_tables = {g.table for g in plan.lists.values()} pure: dict[str, list[list[tuple[Demo, dict[str, Optional[str]]]]]] = {} counts: dict[str, list[int]] = {} @@ -638,7 +759,7 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N ) ) if linked: - g = plan.children[child_tables[ch.table]] + g = next(x for x in _all_groups(plan) if x.table == ch.table) g.changed_columns = sorted( set(g.changed_columns) | set(ch.changed_columns) ) @@ -702,7 +823,8 @@ def plan_pk(schema: Schema, table: str) -> str: def _is_param_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: - for p, g in plan.children.items(): + for g in _demo_groups(plan, d): + p = g.param if g.table != ch.table or d.values.get(p) is None or ch.after is None: continue rel = g.params[p] @@ -726,7 +848,8 @@ def _is_empty_param_row( if ch.after is None or a.after is None: return False - for p, g in plan.children.items(): + for g in _demo_groups(plan, d): + p = g.param if g.table != ch.table or d.values.get(p) is not None: continue if ch.after.get(g.link.column) != a.after.get(g.link.ref_column): @@ -748,7 +871,8 @@ def _is_ref_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: if ch.after is None: return False - for p, g in plan.children.items(): + for g in _demo_groups(plan, d): + p = g.param rel = g.params[p] if not rel.hops or rel.hops[0].table != ch.table or d.values.get(p) is None: continue @@ -782,11 +906,18 @@ def __init__(self, schema: Schema) -> None: self.schema = schema self.d = schema.dialect self.params: dict[str, dict[str, str]] = {} + #: Tables read through a view (the SQL guard refuses their name). + self.views: dict[str, str] = {} self._n = itertools.count() def q(self, name: str) -> str: return store.quote(self.d, name) + def tref(self, table: str) -> str: + """A table as a read names it: its view when it has one.""" + + return self.q(self.views.get(table, table)) + def ph(self, param: str) -> str: for k, v in self.params.items(): if v == {"param": param}: @@ -831,7 +962,7 @@ def path(self, alias: str, table: str, loc: Location, depth: int = 0) -> str: sub = f"a{depth + 1}" inner = self.path(sub, hop.table, rest, depth + 1) return ( - f"(SELECT {inner} FROM {self.q(hop.table)} {sub} WHERE " + f"(SELECT {inner} FROM {self.tref(hop.table)} {sub} WHERE " f"{self.same(f'{sub}.{self.q(hop.ref_column)}', f'{alias}.{self.q(hop.column)}', hop.declared)})" ) @@ -897,7 +1028,7 @@ def copied( self, alias: str, detail: Mapping[str, Any], table: str, column: str ) -> str: src = ( - f"(SELECT s.{self.q(detail['column'])} FROM {self.q(detail['table'])} s " + f"(SELECT s.{self.q(detail['column'])} FROM {self.tref(detail['table'])} s " f"WHERE s.{self.q(detail['ref_column'])} = {alias}.{self.q(detail['via'])})" ) ref = f"{alias}.{self.q(column)}" @@ -1003,6 +1134,7 @@ def build_contract( """One ``openadapt.verify/v1`` document for one field set.""" b = SqlBuilder(schema) + b.views = dict(settings.views) fields = set(variant.fields) anchor = plan.anchor surfaces: dict[str, Surface] = {} @@ -1066,7 +1198,8 @@ def build_contract( found = flavor == "found" groups = [ (p, g) - for p, g in plan.children.items() + for g in _all_groups(plan, variant.dispatch_value) + for p in [g.param] if p in fields or (not found and g.empty_when_null) or (not found and plan.mode == "create" and set(g.co_owners) & fields) @@ -1497,7 +1630,7 @@ def variants( for d in plan.demos: given = { k for k, v in d.values.items() if v is not None and not isinstance(v, list) - } & (located | set(plan.children)) + } & (located | {g.param for g in plan.children.values()}) base_sets.append(frozenset(given)) optional = [p for p in plan.optional if p in located and p not in lists] seen: set[frozenset[str]] = set() @@ -1845,6 +1978,9 @@ def propose( questions: list[Question] = [] _share_locations(schema, plans, kinds) + _break_ties_across(plans) + for p in plans: # child groups of values placed after analysis + _children(schema, p, [], only_missing=True) location_questions: list[Question] = [] # Q: per kind, a value the generator couldn't place. for p in plans: @@ -1855,9 +1991,7 @@ def propose( for name, tied in p.unresolved.items(): pick = chosen.get(name) if isinstance(pick, int) and 0 <= pick < len(tied): - p.locations[name] = tied[pick] - if p.mode == "update" and name not in p.identity + p.changes: - p.changes.append(name) + _resolve(p, name, tied[pick]) else: open_[name] = tied asked = dict(p.unresolved) @@ -2066,6 +2200,10 @@ def propose( "placed_from_another_kind": sorted(p.shared), "demonstrations": len(p.demos), "children": {k: g.table for k, g in p.children.items()}, + "dispatch_children": { + dv: {k: g.table for k, g in gs.items()} + for dv, gs in p.dispatch_children.items() + }, "lists": {k: g.table for k, g in p.lists.items()}, "side_rows": [ {"table": s.table, "count": s.count, "unless": s.unless} From 7ff9b3966f98622c99e652edf89d4e9aabca1a6b Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:44:46 -0400 Subject: [PATCH 57/94] WIP: derived-copy tie-break; logs per kind --- .../generate/verify_contract.py | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 90240c1cea..1cad59aef4 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -359,7 +359,7 @@ def analyze( if len(by_value) > 1: ok = True for dv, per in by_value.items(): - loc, tied = _pick(p, _intersect(per)) + loc, tied = _pick(p, _intersect(per), plan.demos) if loc is None: key = f"{p} (when {plan.dispatch} is {dv})" plan.unresolved[key] = tied @@ -371,7 +371,7 @@ def analyze( plan.changes.append(p) continue all_found = _intersect(found[p]) - loc, tied = _pick(p, all_found) + loc, tied = _pick(p, all_found, plan.demos) if loc is not None: plan.extra[p] = [ x @@ -480,10 +480,13 @@ def _share_locations( def _pick( - param: str, found: Sequence[Location] + param: str, found: Sequence[Location], demos: Sequence[Demo] = () ) -> tuple[Optional[Location], list[Location]]: - """The best location; among tied ones, the one whose column is named - like the parameter (``stage``, not ``last_stage``), if exactly one is.""" + """The best location. Among tied ones: the one whose column is named + like the parameter (``stage``, not ``last_stage``); else drop a column + that is only a copy of another tied one, lower-cased or digits only, in + every row of its table (EspoCRM's ``email_address.lower``, + ``phone_number.numeric``).""" loc, tied = best(found) if loc is not None or not tied: @@ -492,9 +495,38 @@ def _pick( named = [t for t in tied if t.columns and t.columns[-1] == name] if len(named) == 1: return named[0], [] + keep = [t for t in tied if not _derived_copy(t, tied, demos)] + if len(keep) == 1: + return keep[0], [] return None, tied +def _derived_copy( + loc: Location, tied: Sequence[Location], demos: Sequence[Demo] +) -> bool: + if len(loc.columns) != 1 or not loc.hops or not demos: + return False + table = loc.hops[-1].table + rows = demos[0].after.get(table) + if rows is None or rows.count_only is not None or not rows.rows: + return False + + def digits(v: str) -> str: + return "".join(ch for ch in v if ch.isdigit()) + + for other in tied: + if other is loc or other.hops != loc.hops or len(other.columns) != 1: + continue + a, b = other.columns[0], loc.columns[0] + for fn in (str.lower, digits): + if all( + r.get(b) == (fn(r[a]) if r.get(a) is not None else None) + for r in rows.rows.values() + ) and any(r.get(a) != r.get(b) for r in rows.rows.values()): + return True + return False + + def _resolve(plan: KindPlan, key: str, loc: Location) -> None: """Place an unresolved value (answered, or settled by a tie-break).""" @@ -2032,16 +2064,22 @@ def propose( for col, role in _anchor_roles(schema, p, v, actor_params).items(): if role.role == "varies": unchecked.setdefault(p.anchor, set()).add(col) + log_add: set[str] = set() + log_remove: set[str] = set() ans = _answer(answers, "bookkeeping") if isinstance(ans, dict): - logs = (logs | set(ans.get("logs_add", []))) - set(ans.get("logs_remove", [])) + log_add, log_remove = ( + set(ans.get("logs_add", [])), + set(ans.get("logs_remove", [])), + ) + logs = (logs | log_add) - log_remove for t, cols in dict(ans.get("columns_add", {})).items(): book.setdefault(t, set()).update(cols) for t, cols in dict(ans.get("columns_remove", {})).items(): book[t] = book.get(t, set()) - set(cols) sides = sorted({f"{s.table} ({s.count} per write)" for p in plans for s in p.sides}) for p in plans: # a table the person calls a log is no longer counted - p.sides = [s for s in p.sides if s.table not in logs] + p.sides = [s for s in p.sides if s.table not in log_add] if logs or book or unchecked or sides: lines = [] if sides: @@ -2239,7 +2277,8 @@ def propose( continue ok = True for flavor in flavors: - doc = build_contract(schema, p, v, flavor, settings, book, logs) + kind_logs = (p.logs | log_add) - log_remove + doc = build_contract(schema, p, v, flavor, settings, book, kind_logs) rel = Path("contracts") / p.kind / f"{v.slug}.{flavor}.yaml" path = out / rel path.parent.mkdir(parents=True, exist_ok=True) From a7bd394a3fdc1518a1abc57f834011018f51ecac Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:47:08 -0400 Subject: [PATCH 58/94] WIP: identical columns tie-break --- .../generate/verify_contract.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 1cad59aef4..ffd7e168dc 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -498,9 +498,41 @@ def _pick( keep = [t for t in tied if not _derived_copy(t, tied, demos)] if len(keep) == 1: return keep[0], [] + same = _identical_columns(keep, demos) + if same is not None: + return same, [] return None, tied +def _identical_columns( + tied: Sequence[Location], demos: Sequence[Demo] +) -> Optional[Location]: + """Tied columns of one row that hold the same value in every row of + their table can't be told apart by any data: keep the one the table + defines first (the source; a copy is added after it). Either compares + exactly, so a wrong pick stops a write, never passes one.""" + + if not demos or len(tied) < 2: + return None + first = tied[0] + if any( + t.hops != first.hops or len(t.columns) != 1 or t.rule != first.rule + for t in tied + ): + return None + table = first.hops[-1].table if first.hops else None + rows = demos[0].after.get(table) if table else None + if rows is None or rows.count_only is not None or not rows.rows: + return None + cols = [t.columns[0] for t in tied] + if not all(len({r.get(c) for c in cols}) == 1 for r in rows.rows.values()): + return None + order = list(next(iter(rows.rows.values())).keys()) + return min( + tied, key=lambda t: order.index(t.columns[0]) if t.columns[0] in order else 999 + ) + + def _derived_copy( loc: Location, tied: Sequence[Location], demos: Sequence[Demo] ) -> bool: From 25c11b45518ea6a1468791df16f069c717a5a152 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:50:49 -0400 Subject: [PATCH 59/94] WIP: views in their own schema --- .../generate/verify_contract.py | 79 +++++++++++++------ 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index ffd7e168dc..f9463912a7 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -980,7 +980,12 @@ def q(self, name: str) -> str: def tref(self, table: str) -> str: """A table as a read names it: its view when it has one.""" - return self.q(self.views.get(table, table)) + return self.qt(self.views.get(table, table)) + + def qt(self, name: str) -> str: + """A table name, ``schema.table`` quoted part by part.""" + + return ".".join(self.q(part) for part in name.split(".")) def ph(self, param: str) -> str: for k, v in self.params.items(): @@ -1124,7 +1129,7 @@ class Surface: def sql(self, b: SqlBuilder) -> str: cols = ", ".join(f"{expr} AS {b.q(a)}" for a, expr in self.columns) - return f"SELECT {cols} FROM {b.q(self.table)} {self.alias}" + return f"SELECT {cols} FROM {b.qt(self.table)} {self.alias}" def _base_columns(b: SqlBuilder, table: str, alias: str = "t") -> list[tuple[str, str]]: @@ -1959,29 +1964,61 @@ def _sees_triggers(connect: Any) -> bool: return False +def view_schema(schema: Schema) -> str: + """Where the generator's views live: a schema of their own, beside the + app's, so the app's own upgrades and restores never drop them and the + whole-database frame doesn't read them as app tables.""" + + return f"{schema.database}_oa_read" + + +def existing_views( + connect: Any, schema: Schema, tables: Sequence[str] +) -> dict[str, str]: + """Table -> ``schema.view`` for the views the read account can see.""" + + conn = connect() + try: + cur = conn.cursor() + cur.execute( + "SELECT table_name FROM information_schema.views WHERE table_schema = %s", + (view_schema(schema),), + ) + have = {str(r[0]) for r in cur.fetchall()} + finally: + conn.rollback() + conn.close() + return { + t: f"{view_schema(schema)}.oa_read_{t}" + for t in tables + if f"oa_read_{t}" in have + } + + def create_views( admin_read: Mapping[str, Any], schema: Schema, tables: Sequence[str], reader: str ) -> dict[str, str]: - """Create one read-only view per table the SQL guard refuses by name, - in the same database, and grant the read account SELECT on it. Only run - with ``--create-views``; the connection is the admin one given.""" + """Create one read-only view per table the SQL guard refuses by name, in + the schema ``_oa_read``, and grant the read account SELECT on + that schema. Only run with ``--create-views``; the connection is the + admin one given.""" + if schema.dialect != "mysql": + raise GenerateError("--create-views supports MySQL and MariaDB") conn = _admin_connect(admin_read) made = {} + vs = store.quote("mysql", view_schema(schema)) try: cur = conn.cursor() + cur.execute(f"CREATE DATABASE IF NOT EXISTS {vs}") for t in tables: - view = f"oa_read_{t}" - q = store.quote(schema.dialect, t) - v = store.quote(schema.dialect, view) - cur.execute(f"CREATE OR REPLACE VIEW {v} AS SELECT * FROM {q}") - if schema.dialect == "mysql": - cur.execute(f"GRANT SELECT ON {v} TO {reader}") - else: - cur.execute( - f"GRANT SELECT ON {v} TO {store.quote('postgresql', reader)}" - ) - made[t] = view + q = store.quote("mysql", schema.database) + "." + store.quote("mysql", t) + v = vs + "." + store.quote("mysql", f"oa_read_{t}") + cur.execute( + f"CREATE OR REPLACE SQL SECURITY DEFINER VIEW {v} AS SELECT * FROM {q}" + ) + made[t] = f"{view_schema(schema)}.oa_read_{t}" + cur.execute(f"GRANT SELECT ON {vs}.* TO {reader}") conn.commit() finally: conn.close() @@ -2200,18 +2237,16 @@ def propose( } ) if needed: - existing = {t: f"oa_read_{t}" for t in needed if f"oa_read_{t}" in schema.views} + existing = existing_views(connect, schema, needed) missing = [t for t in needed if t not in existing] if missing and create_views_flag: if not admin_read or not reader_account: raise GenerateError( "--create-views needs --admin-read and --reader-account" ) - existing.update(create_views(admin_read, schema, missing, reader_account)) - schema = store.read_schema(connect, dialect) - for t, v in list(existing.items()): - if v not in schema.views: - existing.pop(t) + made = create_views(admin_read, schema, missing, reader_account) + applied.extend(f"view {v} over {t}" for t, v in made.items()) + existing = existing_views(connect, schema, needed) settings.views = existing for p in plans: tables = {p.anchor} | { From a9a5c2aed80a021aea8d0a6c6a4acde07c5d649c Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 15:53:36 -0400 Subject: [PATCH 60/94] WIP: new referenced rows per dispatch value --- .../openadapt_flow/generate/verify_contract.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index f9463912a7..8b4476ba02 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -946,8 +946,16 @@ def _is_ref_row(schema: Schema, plan: KindPlan, d: Demo, ch: RowChange) -> bool: and ch.after.get(col) is not None and _rule_ok(ch.after.get(col), d.values[p], rel.rule) ): + dv = str(d.values.get(plan.dispatch)) if plan.dispatch else None plan.added_refs.setdefault( - p, {"table": ch.table, "column": col, "rule": rel.rule} + f"{p}@{dv}" if dv else p, + { + "table": ch.table, + "column": col, + "rule": rel.rule, + "param": p, + "dv": dv, + }, ) return True return False @@ -1336,8 +1344,9 @@ def build_contract( if plan.mode == "update": target_fields.update(g.changed_columns) # A new row a link row names (a new email address). - for p, ref in plan.added_refs.items(): - if p not in fields or found: + for ref in plan.added_refs.values(): + p = ref["param"] + if p not in fields or found or ref["dv"] not in (None, variant.dispatch_value): continue effects.append( _raw_effect( From 952491ece969daba1f911e16a703bca3d118908f Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:07:32 -0400 Subject: [PATCH 61/94] WIP: replaced linked rows declared with their count; demonstrated sets first; schema column order for ties --- .../openadapt_flow/generate/demo_analysis.py | 2 +- .../generate/verify_contract.py | 80 ++++++++++++++++--- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 7eee2fcf2f..f90ad9a67f 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -374,7 +374,7 @@ def walk( target_row = idx.row(target, v) if target_row is None or (target == anchor.table and hops): continue # back to the main record: not a location - if not declared and not hops and (target, v) in added: + if not declared and not hops and anchor.key and (target, v) in added: # The main row's key columns point at rows that were # there before; a row the write added is reached from a # row that names the main record instead. diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 8b4476ba02..914fe80bab 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -198,6 +198,10 @@ class ChildGroup: #: The parameter whose value the rows hold (``children`` may hold a #: second group for it: a call's lead is its parent and an attendee). param: str = "" + #: An update replaced earlier rows here (removed or changed rows linked + #: to the record), leaving ``after_count`` of them. + replaced: bool = False + after_count: int = 1 @dataclass @@ -359,7 +363,7 @@ def analyze( if len(by_value) > 1: ok = True for dv, per in by_value.items(): - loc, tied = _pick(p, _intersect(per), plan.demos) + loc, tied = _pick(p, _intersect(per), plan.demos, schema) if loc is None: key = f"{p} (when {plan.dispatch} is {dv})" plan.unresolved[key] = tied @@ -371,7 +375,7 @@ def analyze( plan.changes.append(p) continue all_found = _intersect(found[p]) - loc, tied = _pick(p, all_found, plan.demos) + loc, tied = _pick(p, all_found, plan.demos, schema) if loc is not None: plan.extra[p] = [ x @@ -480,7 +484,10 @@ def _share_locations( def _pick( - param: str, found: Sequence[Location], demos: Sequence[Demo] = () + param: str, + found: Sequence[Location], + demos: Sequence[Demo] = (), + schema: Optional[Schema] = None, ) -> tuple[Optional[Location], list[Location]]: """The best location. Among tied ones: the one whose column is named like the parameter (``stage``, not ``last_stage``); else drop a column @@ -498,14 +505,14 @@ def _pick( keep = [t for t in tied if not _derived_copy(t, tied, demos)] if len(keep) == 1: return keep[0], [] - same = _identical_columns(keep, demos) + same = _identical_columns(keep, demos, schema) if same is not None: return same, [] return None, tied def _identical_columns( - tied: Sequence[Location], demos: Sequence[Demo] + tied: Sequence[Location], demos: Sequence[Demo], schema: Optional[Schema] = None ) -> Optional[Location]: """Tied columns of one row that hold the same value in every row of their table can't be told apart by any data: keep the one the table @@ -527,7 +534,9 @@ def _identical_columns( cols = [t.columns[0] for t in tied] if not all(len({r.get(c) for c in cols}) == 1 for r in rows.rows.values()): return None - order = list(next(iter(rows.rows.values())).keys()) + if schema is None or table not in schema.tables: + return None + order = schema.tables[table].column_names return min( tied, key=lambda t: order.index(t.columns[0]) if t.columns[0] in order else 999 ) @@ -810,6 +819,7 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N continue if ch.kind == "removed": if ch.table in child_tables or ch.table in list_tables: + _mark_replaced(schema, plan, d, a, ch) continue plan.removed[ch.table] = plan.removed.get(ch.table, 0) + 1 continue @@ -823,6 +833,7 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N ) ) if linked: + _mark_replaced(schema, plan, d, a, ch) g = next(x for x in _all_groups(plan) if x.table == ch.table) g.changed_columns = sorted( set(g.changed_columns) | set(ch.changed_columns) @@ -881,6 +892,30 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N plan.bookkeeping.setdefault(plan.anchor, set()).update(extra) +def _mark_replaced( + schema: Schema, plan: KindPlan, d: Demo, a: RowChange, ch: RowChange +) -> None: + """An update removed or changed a row linked to the record in a table + that holds a value: count the record's rows there after the write.""" + + if plan.mode != "update" or a.after is None: + return + for g in _demo_groups(plan, d): + if g.table != ch.table: + continue + key = a.after.get(g.link.ref_column) + rows = d.after.get(g.table) + if rows is None or rows.count_only is not None: + continue + consts = [(c, r.value) for c, r in g.roles.items() if r.role == "constant"] + n = sum( + 1 + for r in rows.rows.values() + if r.get(g.link.column) == key and all(r.get(c) == v for c, v in consts) + ) + g.replaced, g.after_count = True, n + + def plan_pk(schema: Schema, table: str) -> str: pk = schema.tables[table].primary_key return pk[0] if len(pk) == 1 else "" @@ -1341,6 +1376,24 @@ def build_contract( # Found: the row is already there, and nothing changes. effect["count_new_only"] = True effects.append(effect) + if plan.mode == "update" and g.replaced and not found: + # The demonstrations replaced the record's earlier rows here (an + # old email address link removed): afterwards the record has + # exactly as many such rows as they left, and its earlier ones + # may go. + m = { + k: v + for k, v in effects[-1]["match"].items() + if k.startswith(("main__", "pin__")) or k in g.roles + } + effects.append( + { + "kind": "record_written", + "probe": f"surface={sname}|the record's rows here after the write", + "match": m, + "expected_count": g.after_count, + } + ) if plan.mode == "update": target_fields.update(g.changed_columns) # A new row a link row names (a new email address). @@ -1711,12 +1764,15 @@ def variants( } & (located | {g.param for g in plan.children.values()}) base_sets.append(frozenset(given)) optional = [p for p in plan.optional if p in located and p not in lists] - seen: set[frozenset[str]] = set() - sets: list[frozenset[str]] = [] - for base in base_sets: - drop = [p for p in optional if p in base] - for n in range(0, len(drop) + 1): - for combo in itertools.combinations(drop, n): + seen: set[frozenset[str]] = set(base_sets) + # Every demonstrated set first, then sets with fewer optional fields, + # one field left out at a time across demonstrations, so the cap keeps + # the sets closest to what was shown. + sets: list[frozenset[str]] = list(dict.fromkeys(base_sets)) + drops = {base: [p for p in optional if p in base] for base in base_sets} + for n in range(1, max((len(v) for v in drops.values()), default=0) + 1): + for base in base_sets: + for combo in itertools.combinations(drops[base], n): s = frozenset(base - set(combo)) if s not in seen: seen.add(s) From 0828bdfe8d71a807cb93919af8354e708f48d5d9 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:12:19 -0400 Subject: [PATCH 62/94] WIP: empty pins only where the app left the column empty; generated tokens unchecked; co-owners not via the main record --- .../generate/verify_contract.py | 78 ++++++++++++++----- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 914fe80bab..1e1c904fbe 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -691,7 +691,11 @@ def _children( and not isinstance(d.values.get(q), (list, dict)) ] if hits and all( - h and h[0].hops and _hop_cost(h[0].hops[0]) <= 1 for h in hits + h + and h[0].hops + and _hop_cost(h[0].hops[0]) <= 1 + and h[0].hops[0].table != plan.anchor + for h in hits ): co.append(q) explained |= {h[0].hops[0].column for h in hits} @@ -1280,20 +1284,31 @@ def build_contract( roles = _anchor_roles(schema, plan, variant, settings.actor_params) for col, role in sorted(roles.items()): _pin(b, main, anchor, col, role, match, settings.actor_params) + support = _support(plan, variant) for p in plan.optional: if p in fields or p in variant.lists: continue loc = plan.locations.get(p) if loc is None: continue - if not loc.hops: - for c in loc.columns: - main.columns.append((f"empty__{c}", b.empty("t", anchor, c))) - match[f"empty__{c}"] = {"literal": "1"} - elif loc.hops[0].kind == "fk": - c = loc.hops[0].column - main.columns.append((f"empty__{c}", b.empty("t", anchor, c))) - match[f"empty__{c}"] = {"literal": "1"} + cols = ( + list(loc.columns) + if not loc.hops + else ([loc.hops[0].column] if loc.hops[0].kind == "fk" else []) + ) + for c in cols: + # A column another value of this field set fills (a call's + # parent is its account, or its lead) is not pinned empty. + if any( + (a.after or {}).get(c) not in (None, "") + for d, a in support + if d.values.get(p) is None + ): + continue + alias = _add_column( + main, f"empty__{_alias(c)}", b.empty("t", anchor, c) + ) + match[alias] = {"literal": "1"} effect: dict[str, Any] = { "kind": "record_written", "probe": "surface=record|the main record holds the task's values", @@ -1698,14 +1713,7 @@ def _anchor_roles( """The main row's columns no parameter explains, from the demonstrations that give this field set (all of them when none gives exactly it).""" - fields = set(variant.fields) | set(variant.lists) - if variant.dispatch_value is not None: - fields.add(plan.dispatch or "") - support = [ - (d, a) - for d, a in zip(plan.demos, plan.anchors) - if fields <= {k for k, v in d.values.items() if v is not None} - ] or list(zip(plan.demos, plan.anchors)) + support = _support(plan, variant) explained: set[str] = set() for loc in plan.locations.values(): if not loc.hops: @@ -1713,7 +1721,41 @@ def _anchor_roles( elif loc.hops[0].kind == "fk": explained.add(loc.hops[0].column) rows = [(d, a.after or {}) for d, a in support] - return column_roles(schema, plan.anchor, rows, explained, actor_params) + roles = column_roles(schema, plan.anchor, rows, explained, actor_params) + if len(support) < len(plan.demos): + # A column that differs between all demonstrations and holds a + # generated token (a UUID) is the app's own, whichever ones support + # this field set. + everyone = [(d, a.after or {}) for d, a in zip(plan.demos, plan.anchors)] + for col, role in column_roles( + schema, plan.anchor, everyone, explained, actor_params + ).items(): + values = [r.get(col) for _, r in everyone] + if role.role == "varies" and all( + v is not None and _TOKEN.match(v) for v in values + ): + roles[col] = role + return roles + + +_TOKEN = re.compile( + r"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + r"|[0-9a-fA-F]{16,})$" +) + + +def _support(plan: KindPlan, variant: Variant) -> list[tuple[Demo, RowChange]]: + """The demonstrations that give every field of this set (all of them + when none does).""" + + fields = set(variant.fields) | set(variant.lists) + if variant.dispatch_value is not None: + fields.add(plan.dispatch or "") + return [ + (d, a) + for d, a in zip(plan.demos, plan.anchors) + if fields <= {k for k, v in d.values.items() if v is not None} + ] or list(zip(plan.demos, plan.anchors)) # -- variants ----------------------------------------------------------------------- From f2e9a8e728fb38def6b32ab5eb03d35b079a1fa6 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:24:35 -0400 Subject: [PATCH 63/94] docs: generator decision rules --- packages/flow/docs/GENERATE_CONTRACTS.md | 65 ++++++++++++++++++------ 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 821f0597cb..4db99d3513 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -126,23 +126,43 @@ that as a stop: the write must not be sent unchecked. | A display column of the row a key column points to | `project_id` -> `projects.name` | | Two text columns joined by one space | `firstname` and `lastname` | | A row the write added that holds the main record's key | a journal note, a custom field value | +| A row it added that links the record to the row holding the value | an email address, a call's attendee | A key column can be a declared foreign key, or a column whose value is another table's primary key. When several places fit in every demonstration, the generator keeps the shortest path, then a declared key, then a column -named after its table, then the most exact rule. If places are still tied, -that is a question. - -An update parameter that no example gives, such as `new_zip`, is placed at the -main table's column with the same name after a leading `new_` or `set_`, when -there is one. The report lists such parameters under `placed_by_name`. +named after its table, then the most exact rule. Among places still tied, it +keeps, in this order: + +1. the column named like the parameter (`stage`, not `last_stage`); +2. the source column when another is its lower-cased or digits-only copy in + every row (`email_address.name`, not `email_address.lower`), or the column + the table defines first when they hold the same value in every row; +3. the place another kind's demonstrations used for the same table. + +If places are still tied, that is a question. A value that is also in a row +the write added beside the record (a call's lead is its parent and an +attendee row) is checked in both places. + +A parameter that no example gives is placed where another kind's +demonstrations found the parameter of the same name on the same main table +(`placed_from_another_kind`), else at the main table's column of the same +name after a leading `new_` or `set_` (`placed_by_name`). A parameter whose +value names the field to change (`field: status`, `value: In Process`) gets a +contract per field: the demonstrated ones, and every other text column of the +main table. **Field sets.** A task's field set is its non-null values. For an update, the generator writes a contract for every non-empty set of the fields to change that it found, up to 64. For a create, it writes one per demonstrated set, -plus the sets with an optional field (null in some example) left out. A -field left out of a create must be empty in the new record. A list parameter -gets a contract per length, from 1 to 10 items. +then the sets with optional fields left out, up to 64 in all. A field is +optional when some example leaves it null or its place can be empty (a +nullable column, a row beside the record). A field left out of a create must +be empty in the new record, unless the demonstrations that left it out +filled that column anyway (a call's parent, which is its account when there +is no lead). A row the app adds with an empty value when a field is left out +(Redmine's custom field values, a journal with no note) is expected then +too. A list parameter gets a contract per length, from 1 to 10 items. **Create, found, update.** @@ -154,15 +174,27 @@ gets a contract per length, from 1 to 10 items. record with the task's values is already there: a "make sure it exists" task whose record exists, or an update that is already in place. - `update`: exactly one record matches the task's identity values and holds - the new values. Only the fields the task changes may change. + the new values. Only the fields the task changes may change. When the + demonstrations replaced the record's rows beside it (an old email address + link removed), the earlier rows may go, and afterwards the record has as + many such rows as the demonstrations left. + +**Columns of a new row no value explains.** A key, a time stamp from the +write, the acting user, a link to a row the write added, the year, month, +week or day of a date the task gives, a generated token (a UUID), or a copy +of a column of the row a key points to (an invoice line's price from its +product) is recognized as such. Free text the demonstrations left empty is +not checked. Any other column must hold the value the demonstrations +showed; one that differed between them is not checked and is listed in the +bookkeeping question. **Rows the app adds on its own.** A row type the app added the same number of times in every demonstration (an agenda event, a stream note, a price-history row) is declared with that count and its constant columns. A table whose added rows vary between demonstrations, or whose name says log, history, -audit or similar, is proposed as a log. Columns that changed with no task -value (a modified time, a version counter) are proposed as bookkeeping. -Both go in one question. +audit or similar, is proposed as a log of that kind. Columns that changed +with no task value (a modified time, a version counter) are proposed as +bookkeeping. All of it goes in one question. **The frame.** On MySQL and MariaDB every contract uses the whole-database frame (`frame.database`): a row added, removed or changed in any table that @@ -189,8 +221,11 @@ and an admin connection you give, or not written, with the reason. needs it only to see them. - `--create-views --admin-read admin.yaml --reader-account ...`: the verifier's SQL guard refuses some table names in a read (EspoCRM's `call`). - This creates a view `oa_read_
` and grants the read account `SELECT` - on it. Without the flag, the kind is refused with the reason. + This creates a view `oa_read_
` in a schema of its own, + `_oa_read` (so the app's own restores and upgrades don't drop it), + and grants the read account `SELECT` on that schema. Without the flag, the + kind is refused with the reason. `report.json` lists what was applied under + `applied`. Every contract is loaded and read once against the live store before it is written out. A contract whose read fails is not written; `report.json` says From d39f04bec0d99844f25f7c0535f3673083979138 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:34:01 -0400 Subject: [PATCH 64/94] WIP: created rows' tables append-only in the whole-database frame, so the recheck sees them --- packages/flow/openadapt_flow/generate/verify_contract.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 1e1c904fbe..8992e467ba 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1483,6 +1483,15 @@ def build_contract( continue allow.append({"table": t}) for t in sorted(named_tables): + if plan.mode == "create" or flavor == "found": + # The contract's own read judges the rows the write adds; the + # whole-database frame still sees any existing row change, + # now and at the recheck (a late edit to a look-alike). + allow.append({"table": t}) + continue + # An update changes a row here, which an append-only table + # can't allow: its own read judges this table, and the recheck + # (database only) doesn't see it. allow.append( { "table": t, From 1756cb556264ff8a3beee43004babfba402a2273 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:53:44 -0400 Subject: [PATCH 65/94] WIP: email and phone values always compared in canonical form; docs link --- packages/flow/docs/VERIFY_WRITES.md | 5 +++++ .../openadapt_flow/generate/demo_analysis.py | 20 ++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/flow/docs/VERIFY_WRITES.md b/packages/flow/docs/VERIFY_WRITES.md index 5bcc461af6..f71443376a 100644 --- a/packages/flow/docs/VERIFY_WRITES.md +++ b/packages/flow/docs/VERIFY_WRITES.md @@ -26,6 +26,11 @@ effects: The contract belongs to whoever runs the agent. The agent can't change what it is checked against. +When the app keeps its data in MySQL, MariaDB or PostgreSQL, the generator +writes these contracts from the schema and one demonstration write per +example, and asks a person only what it can't decide: +[GENERATE_CONTRACTS.md](GENERATE_CONTRACTS.md). + ## 2. Wrap the write In Python, wrap your agent's own write function. The same code works as a tool in the Claude Agent SDK, OpenAI Agents, LangGraph, or a plain script: diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index f90ad9a67f..eb576f8158 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -47,6 +47,8 @@ _IDLIKE = re.compile(r"(^id$|_id$|^id_|^fk_|_fk$|^parent$|^parent_id$)", re.I) _IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +_EMAIL = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") +_PHONE = re.compile(r"^\+?[0-9][0-9 ().\-/]*$") #: Table names an app's own logs usually carry. Only a proposal: a person #: confirms which tables are logs (the bookkeeping question). _LOG_NAME = re.compile( @@ -99,19 +101,23 @@ def rule_for(stored: Optional[str], column_kind: str, value: Any) -> Optional[st text = value_text(value) if text == "": return None + if column_kind == "text": + # An email address or a phone number is compared in its declared + # canonical form even when this demonstration's text matches + # exactly: another write may store ``Ana@Example.org`` or + # ``+16175550144`` for the same value. + if _EMAIL.match(text) and stored.casefold() == text.casefold(): + return "email" + if _PHONE.match(text) and sum(ch.isdigit() for ch in text) >= 6: + a, b = kind_form("phone", stored), kind_form("phone", text) + if a is not None and a == b: + return "phone" if stored == text and column_kind not in ("number", "bool", "date", "datetime"): # A number or date column is compared by value even when this # demonstration's text happens to match: another row of the same # column may print as ``5.5000``. return "exact" if column_kind == "text": - if "@" in text and stored.casefold() == text.casefold(): - return "email" - digits = sum(ch.isdigit() for ch in text) - if digits >= 6: - a, b = kind_form("phone", stored), kind_form("phone", text) - if a is not None and a == b: - return "phone" return None if column_kind in ("number", "bool"): a, b = _decimal(stored), _decimal(text) From fde99bf3809b86ca49b9392d6e419fa3e957a9e0 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 16:57:02 -0400 Subject: [PATCH 66/94] WIP: answered location questions still counted --- packages/flow/docs/GENERATE_CONTRACTS.md | 4 ++-- packages/flow/openadapt_flow/generate/verify_contract.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 4db99d3513..0b6d28a41c 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -119,8 +119,8 @@ that as a stop: the write must not be sent unchecked. | Rule | Example | | --- | --- | | The stored text equals the value | `subject` | -| Equal after case folding (an email address) | `Ana@Example.org` | -| Equal digits (a phone number) | `02 38 53 21 09` stored as `0238532109` | +| Equal after case folding, for any value shaped like an email address | `Ana@Example.org` | +| Equal digits, for any value shaped like a phone number | `02 38 53 21 09` stored as `0238532109` | | Equal as a number, a date or a date-time, compared in SQL | `9.20` stored as `9.2`; `2026-10-27 14:30` stored as `2026-10-27 14:30:00` | | Minutes between two date-time columns of the row | a call's duration | | A display column of the row a key column points to | `project_id` -> `projects.name` | diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 8992e467ba..612ac09b51 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -2196,22 +2196,20 @@ def propose( _share_locations(schema, plans, kinds) _break_ties_across(plans) - for p in plans: # child groups of values placed after analysis - _children(schema, p, [], only_missing=True) location_questions: list[Question] = [] # Q: per kind, a value the generator couldn't place. for p in plans: if not p.unresolved: continue chosen = dict(_answer(answers, f"locations.{p.kind}") or {}) + asked = dict(p.unresolved) open_ = {} - for name, tied in p.unresolved.items(): + for name, tied in list(p.unresolved.items()): pick = chosen.get(name) if isinstance(pick, int) and 0 <= pick < len(tied): _resolve(p, name, tied[pick]) else: open_[name] = tied - asked = dict(p.unresolved) p.unresolved = open_ if asked: parts = [] @@ -2328,6 +2326,8 @@ def propose( DEFAULT_RECHECK_S, ) ) + for p in plans: # child groups of values placed after analysis + _children(schema, p, [], only_missing=True) questions.extend(location_questions) questions_open = [q for q in questions if _answer(answers, q.key) is None] settings = Settings( From eb8fee67006deed7cd34a44f68359e4f77c3711f Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 17:16:05 -0400 Subject: [PATCH 67/94] WIP: Dolibarr results --- .../results/dolibarr/QUESTIONS.md | 6 +- ...l+name+phone+town+zip.8386ba67.create.yaml | 6 +- ...il+name+phone+town+zip.8386ba67.found.yaml | 3 +- ...ice_date+town+linesx2.17de9df9.create.yaml | 44 +- ...oice_date+town+linesx2.17de9df9.found.yaml | 81 ++- ...customer+town+linesx2.c91f9518.create.yaml | 484 -------------- .../customer+town+linesx2.c91f9518.found.yaml | 88 --- ...excl_vat+ref+vat_rate.bcd72fd0.create.yaml | 234 +++++-- ..._excl_vat+ref+vat_rate.bcd72fd0.found.yaml | 69 +- ...w_email+new_phone+town.be743cce.found.yaml | 101 --- ..._email+new_phone+town.be743cce.update.yaml | 218 ------ ...w_email+new_phone+town.110f7554.found.yaml | 3 +- ..._email+new_phone+town.110f7554.update.yaml | 4 +- .../results/dolibarr/manifest.yaml | 625 ++++++++++++++++++ .../results/dolibarr/report.json | 28 +- 15 files changed, 969 insertions(+), 1025 deletions(-) delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md index 92a0749f46..336f371750 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md @@ -1,6 +1,6 @@ # Questions for a person -1. Is this the app's own bookkeeping, which a write may change without a task asking? Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. +1. Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): llx_actioncomm (1 per write), llx_actioncomm_resources (1 per write). Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. - Answer key: `bookkeeping`. Proposed: `accept`. - Answered: `"accept"` @@ -14,7 +14,7 @@ ## Kinds -- `create_customer` (create, main table `llx_societe`): 1 contract(s). -- `create_product` (create, main table `llx_product`): 1 contract(s). +- `create_customer` (create, main table `llx_societe`): 64 contract(s). +- `create_product` (create, main table `llx_product`): 4 contract(s). - `create_draft_invoice` (create, main table `llx_facture`): 10 contract(s). - `update_customer` (update, main table `llx_societe`): 15 contract(s). diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml index 095d9bcd5b..bf2b55e636 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml @@ -151,6 +151,7 @@ effects: param: country email: param: email + kind: email nom: param: name phone: @@ -255,8 +256,6 @@ effects: - kind: record_written probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm match: - code: - literal: AC_COMPANY_CREATE entity: literal: '1' event_paid: @@ -308,8 +307,6 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_societe - mode: skip - reason: read in full, every column, by this contract's own surface views: llx_actioncomm: raw_llx_actioncomm llx_actioncomm_resources: raw_llx_actioncomm_resources @@ -317,6 +314,7 @@ frame: settle_attempts: 5 new_row_fields: llx_actioncomm: + - code - datec - datep - datep2 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml index c54cbd55ef..c00bc365fc 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml @@ -74,6 +74,7 @@ effects: param: country email: param: email + kind: email nom: param: name phone: @@ -98,8 +99,6 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_societe - mode: skip - reason: read in full, every column, by this contract's own surface views: {} settle_s: 1 settle_attempts: 5 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml index 46aa29ec26..f2931d37f3 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml @@ -169,44 +169,8 @@ verifier: `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, CASE WHEN CAST(%(v2)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_1_qty`, CASE WHEN - t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE - WHEN t.`date_end` IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS - NULL THEN '1' ELSE '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_multicurrency` = (SELECT - s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' - END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE '0' END AS - `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, - CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, - CASE WHEN t.`fk_remise_except` IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE - WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` - = (SELECT s.`fk_user_author` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE - '0' END AS `pin__fk_user_author`, CASE WHEN t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__info_bits`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, - CASE WHEN t.`multicurrency_subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` - = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE - WHEN t.`price` IS NULL THEN '1' ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT - s.`type` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, - CASE WHEN t.`rang` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN - t.`remise` = (SELECT s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' - ELSE '0' END AS `pin__remise`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__situation_percent`, CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__special_code`, CASE WHEN t.`subprice` = (SELECT s.`price` FROM - `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__subprice`, CASE - WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__subprice_ttc`, - CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) - THEN '1' ELSE '0' END AS `pin__total_localtax1`, CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax2`, - CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) - THEN '1' ELSE '0' END AS `pin__tva_tx` FROM `llx_facturedet` t + AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_1_qty` FROM `llx_facturedet` + t sql_query_params: v0: *id002 v1: @@ -480,11 +444,7 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_facture - mode: skip - reason: read in full, every column, by this contract's own surface - table: llx_facturedet - mode: skip - reason: read in full, every column, by this contract's own surface views: {} settle_s: 1 settle_attempts: 5 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml index e9e6d0f41e..7db480bfb8 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml @@ -54,8 +54,56 @@ verifier: = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, (SELECT CAST(a1.`town` AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town` FROM `llx_facture` t sql_query_params: - v0: + v0: &id002 param: invoice_date + rows_llx_facturedet: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_facture` + AS CHAR) AS `fk_facture`, CAST(t.`fk_parent_line` AS CHAR) AS `fk_parent_line`, CAST(t.`fk_product` + AS CHAR) AS `fk_product`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS + `description`, CAST(t.`vat_src_code` AS CHAR) AS `vat_src_code`, CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, + CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, + CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, + CAST(t.`qty` AS CHAR) AS `qty`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise` + AS CHAR) AS `remise`, CAST(t.`fk_remise_except` AS CHAR) AS `fk_remise_except`, CAST(t.`subprice` + AS CHAR) AS `subprice`, CAST(t.`subprice_ttc` AS CHAR) AS `subprice_ttc`, CAST(t.`price` AS CHAR) + AS `price`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, + CAST(t.`total_localtax1` AS CHAR) AS `total_localtax1`, CAST(t.`total_localtax2` AS CHAR) AS `total_localtax2`, + CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, CAST(t.`product_type` AS CHAR) AS `product_type`, + CAST(t.`date_start` AS CHAR) AS `date_start`, CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`info_bits` + AS CHAR) AS `info_bits`, CAST(t.`buy_price_ht` AS CHAR) AS `buy_price_ht`, CAST(t.`fk_product_fournisseur_price` + AS CHAR) AS `fk_product_fournisseur_price`, CAST(t.`batch` AS CHAR) AS `batch`, CAST(t.`fk_warehouse` + AS CHAR) AS `fk_warehouse`, CAST(t.`special_code` AS CHAR) AS `special_code`, CAST(t.`rang` AS + CHAR) AS `rang`, CAST(t.`fk_contract_line` AS CHAR) AS `fk_contract_line`, CAST(t.`fk_unit` AS + CHAR) AS `fk_unit`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_code_ventilation` + AS CHAR) AS `fk_code_ventilation`, CAST(t.`situation_percent` AS CHAR) AS `situation_percent`, + CAST(t.`fk_prev_id` AS CHAR) AS `fk_prev_id`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, + CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, + CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_subprice` + AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, + CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` + AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CAST(a2.`nom` AS CHAR) FROM `llx_societe` + a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS + `main__customer`, CASE WHEN (SELECT a1.`datef` FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) + = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `main__invoice_date`, (SELECT (SELECT CAST(a2.`town` + AS CHAR) FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` + = t.`fk_facture`) AS `main__town`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE + CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_0_product_ref`, CASE + WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v1)s AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `p__lines_0_qty`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 + WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, + CASE WHEN CAST(%(v2)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `p__lines_1_qty` FROM `llx_facturedet` t + sql_query_params: + v0: *id002 + v1: + param: lines.0.qty + v2: + param: lines.1.qty effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -67,6 +115,34 @@ effects: p__town: param: town expected_count: 1 +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__invoice_date: + literal: '1' + main__town: + param: town + p__lines_0_product_ref: + param: lines.0.product_ref + p__lines_0_qty: + literal: '1' + expected_count: 1 +- kind: record_written + probe: surface=rows_llx_facturedet|a row beside the main record holds lines + match: + main__customer: + param: customer + main__invoice_date: + literal: '1' + main__town: + param: town + p__lines_1_product_ref: + param: lines.1.product_ref + p__lines_1_qty: + literal: '1' + expected_count: 1 frame: ignore_fields: - fk_user_modif @@ -80,8 +156,7 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_facture - mode: skip - reason: read in full, every column, by this contract's own surface + - table: llx_facturedet views: {} settle_s: 1 settle_attempts: 5 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml deleted file mode 100644 index 18d772fe81..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.create.yaml +++ /dev/null @@ -1,484 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, create, fields customer, town, linesx2. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` - AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, - CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` - AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, - CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` - AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, - CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` - AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) - AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` - AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) - AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS - `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, - CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, - CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, - CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, - CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, - CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, - CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` - AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` - AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, - CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS - `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS - `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) - AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` - AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` - AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, - CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS - `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, - CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, - CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, - CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) - AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, - CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` - AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, (SELECT CAST(a1.`town` - AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town`, CASE WHEN t.`close_missing_amount` - IS NULL THEN '1' ELSE '0' END AS `pin__close_missing_amount`, CASE WHEN t.`date_closing` IS NULL - THEN '1' ELSE '0' END AS `pin__date_closing`, CASE WHEN t.`date_pointoftax` IS NULL THEN '1' ELSE - '0' END AS `pin__date_pointoftax`, CASE WHEN t.`date_valid` IS NULL THEN '1' ELSE '0' END AS `pin__date_valid`, - CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE - WHEN t.`fk_account` IS NULL THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_cond_reglement` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_cond_reglement`, CASE WHEN t.`fk_fac_rec_source` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_fac_rec_source`, CASE WHEN t.`fk_facture_source` IS - NULL THEN '1' ELSE '0' END AS `pin__fk_facture_source`, CASE WHEN t.`fk_incoterms` = (SELECT s.`fk_incoterms` - FROM `llx_societe` s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_incoterms`, - CASE WHEN t.`fk_mode_reglement` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_mode_reglement`, - CASE WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_societe` s WHERE s.`rowid` - = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_projet` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_projet`, CASE WHEN t.`fk_statut` = (SELECT s.`statut` FROM `llx_societe` - s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_statut`, CASE WHEN t.`fk_transport_mode` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_transport_mode`, CASE WHEN t.`fk_user_closing` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_user_closing`, CASE WHEN t.`fk_user_modif` IS NULL THEN '1' - ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_user_valid` IS NULL THEN '1' ELSE '0' END - AS `pin__fk_user_valid`, CASE WHEN t.`is_also_delivery_note` = CAST('0' AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `pin__is_also_delivery_note`, CASE WHEN t.`localtax1` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax1`, CASE WHEN t.`localtax2` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax2`, CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, CASE WHEN t.`paye` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__paye`, CASE WHEN t.`prorata_discount` IS NULL THEN '1' ELSE '0' - END AS `pin__prorata_discount`, CASE WHEN t.`remise` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE - '0' END AS `pin__remise`, CASE WHEN t.`remise_absolue` = CAST('0' AS DECIMAL(65,30)) THEN '1' - ELSE '0' END AS `pin__remise_absolue`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`retained_warranty` = CAST('0' AS - DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty`, CASE WHEN t.`retained_warranty_date_limit` - IS NULL THEN '1' ELSE '0' END AS `pin__retained_warranty_date_limit`, CASE WHEN t.`retained_warranty_fk_cond_reglement` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty_fk_cond_reglement`, - CASE WHEN t.`revenuestamp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__revenuestamp`, - CASE WHEN t.`situation_counter` IS NULL THEN '1' ELSE '0' END AS `pin__situation_counter`, CASE - WHEN t.`situation_cycle_ref` IS NULL THEN '1' ELSE '0' END AS `pin__situation_cycle_ref`, CASE - WHEN t.`situation_final` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_final`, - CASE WHEN t.`subtype` IS NULL THEN '1' ELSE '0' END AS `pin__subtype`, CASE WHEN t.`type` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__type` FROM `llx_facture` t - rows_llx_facturedet: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_facture` - AS CHAR) AS `fk_facture`, CAST(t.`fk_parent_line` AS CHAR) AS `fk_parent_line`, CAST(t.`fk_product` - AS CHAR) AS `fk_product`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS - `description`, CAST(t.`vat_src_code` AS CHAR) AS `vat_src_code`, CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, - CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, - CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, - CAST(t.`qty` AS CHAR) AS `qty`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise` - AS CHAR) AS `remise`, CAST(t.`fk_remise_except` AS CHAR) AS `fk_remise_except`, CAST(t.`subprice` - AS CHAR) AS `subprice`, CAST(t.`subprice_ttc` AS CHAR) AS `subprice_ttc`, CAST(t.`price` AS CHAR) - AS `price`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, - CAST(t.`total_localtax1` AS CHAR) AS `total_localtax1`, CAST(t.`total_localtax2` AS CHAR) AS `total_localtax2`, - CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, CAST(t.`product_type` AS CHAR) AS `product_type`, - CAST(t.`date_start` AS CHAR) AS `date_start`, CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`info_bits` - AS CHAR) AS `info_bits`, CAST(t.`buy_price_ht` AS CHAR) AS `buy_price_ht`, CAST(t.`fk_product_fournisseur_price` - AS CHAR) AS `fk_product_fournisseur_price`, CAST(t.`batch` AS CHAR) AS `batch`, CAST(t.`fk_warehouse` - AS CHAR) AS `fk_warehouse`, CAST(t.`special_code` AS CHAR) AS `special_code`, CAST(t.`rang` AS - CHAR) AS `rang`, CAST(t.`fk_contract_line` AS CHAR) AS `fk_contract_line`, CAST(t.`fk_unit` AS - CHAR) AS `fk_unit`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_code_ventilation` - AS CHAR) AS `fk_code_ventilation`, CAST(t.`situation_percent` AS CHAR) AS `situation_percent`, - CAST(t.`fk_prev_id` AS CHAR) AS `fk_prev_id`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, - CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, - CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_subprice` - AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, - CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` - AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CAST(a2.`nom` AS CHAR) FROM `llx_societe` - a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS - `main__customer`, (SELECT (SELECT CAST(a2.`town` AS CHAR) FROM `llx_societe` a2 WHERE a2.`rowid` - = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS `main__town`, (SELECT - CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` - AS BINARY)) AS `p__lines_0_product_ref`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND t.`qty` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_0_qty`, CASE WHEN - t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE - WHEN t.`date_end` IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS - NULL THEN '1' ELSE '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_multicurrency` = (SELECT - s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' - END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE '0' END AS - `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, - CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, - CASE WHEN t.`fk_remise_except` IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE - WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` - = (SELECT s.`fk_user_author` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE - '0' END AS `pin__fk_user_author`, CASE WHEN t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__info_bits`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, - CASE WHEN t.`multicurrency_subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` - = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE - WHEN t.`price` IS NULL THEN '1' ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT - s.`type` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, - CASE WHEN t.`rang` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN - t.`remise` = (SELECT s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' - ELSE '0' END AS `pin__remise`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__situation_percent`, CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__special_code`, CASE WHEN t.`subprice` = (SELECT s.`price` FROM - `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__subprice`, CASE - WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__subprice_ttc`, - CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) - THEN '1' ELSE '0' END AS `pin__total_localtax1`, CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax2`, - CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) - THEN '1' ELSE '0' END AS `pin__tva_tx`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE - CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, CASE - WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v1)s AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `p__lines_1_qty`, CASE WHEN t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE WHEN t.`date_end` IS NULL THEN '1' ELSE '0' - END AS `pin__date_end`, CASE WHEN t.`date_start` IS NULL THEN '1' ELSE '0' END AS `pin__date_start`, - CASE WHEN t.`fk_code_ventilation` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, - CASE WHEN t.`fk_contract_line` IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE - WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` - = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_prev_id`, CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, CASE WHEN t.`fk_remise_except` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE - '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` FROM `llx_facture` - s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, CASE WHEN - t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_user_modif`, - CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_warehouse`, - CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__info_bits`, - CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_tx`, - CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_tx`, - CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` FROM `llx_facture` s WHERE s.`rowid` - = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, CASE WHEN t.`multicurrency_subprice` - = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END - AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE WHEN t.`price` IS NULL THEN '1' - ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT s.`type` FROM `llx_facture` - s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, CASE WHEN t.`rang` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN t.`remise` = (SELECT - s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__remise`, - CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_percent`, - CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_percent`, - CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__special_code`, - CASE WHEN t.`subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) - THEN '1' ELSE '0' END AS `pin__subprice`, CASE WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__subprice_ttc`, CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax1`, - CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) - THEN '1' ELSE '0' END AS `pin__total_localtax2`, CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM - `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx` FROM - `llx_facturedet` t - sql_query_params: - v0: - param: lines.0.qty - v1: - param: lines.1.qty -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__customer: - param: customer - p__town: - param: town - pin__close_missing_amount: - literal: '1' - pin__date_closing: - literal: '1' - pin__date_pointoftax: - literal: '1' - pin__date_valid: - literal: '1' - pin__entity: - literal: '1' - pin__fk_account: - literal: '1' - pin__fk_cond_reglement: - literal: '1' - pin__fk_fac_rec_source: - literal: '1' - pin__fk_facture_source: - literal: '1' - pin__fk_incoterms: - literal: '1' - pin__fk_mode_reglement: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_projet: - literal: '1' - pin__fk_statut: - literal: '1' - pin__fk_transport_mode: - literal: '1' - fk_user_author: - param: bot_user_id - pin__fk_user_closing: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_user_valid: - literal: '1' - pin__is_also_delivery_note: - literal: '1' - pin__localtax1: - literal: '1' - pin__localtax2: - literal: '1' - multicurrency_code: - literal: EUR - pin__multicurrency_tx: - literal: '1' - pin__paye: - literal: '1' - pin__prorata_discount: - literal: '1' - pin__remise: - literal: '1' - pin__remise_absolue: - literal: '1' - pin__remise_percent: - literal: '1' - pin__retained_warranty: - literal: '1' - pin__retained_warranty_date_limit: - literal: '1' - pin__retained_warranty_fk_cond_reglement: - literal: '1' - pin__revenuestamp: - literal: '1' - pin__situation_counter: - literal: '1' - pin__situation_cycle_ref: - literal: '1' - pin__situation_final: - literal: '1' - pin__subtype: - literal: '1' - pin__type: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_llx_facturedet|a row beside the main record holds lines - match: - main__customer: - param: customer - main__town: - param: town - p__lines_0_product_ref: - param: lines.0.product_ref - p__lines_0_qty: - literal: '1' - pin__buy_price_ht: - literal: '1' - pin__date_end: - literal: '1' - pin__date_start: - literal: '1' - pin__fk_code_ventilation: - literal: '1' - pin__fk_contract_line: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_parent_line: - literal: '1' - pin__fk_prev_id: - literal: '1' - pin__fk_product_fournisseur_price: - literal: '1' - pin__fk_remise_except: - literal: '1' - pin__fk_unit: - literal: '1' - pin__fk_user_author: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_warehouse: - literal: '1' - pin__info_bits: - literal: '1' - pin__localtax1_tx: - literal: '1' - localtax1_type: - literal: '0' - pin__localtax2_tx: - literal: '1' - localtax2_type: - literal: '0' - pin__multicurrency_code: - literal: '1' - pin__multicurrency_subprice: - literal: '1' - pin__multicurrency_subprice_ttc: - literal: '1' - pin__price: - literal: '1' - pin__product_type: - literal: '1' - pin__rang: - literal: '1' - pin__remise: - literal: '1' - pin__remise_percent: - literal: '1' - pin__situation_percent: - literal: '1' - pin__special_code: - literal: '1' - pin__subprice: - literal: '1' - pin__subprice_ttc: - literal: '1' - pin__total_localtax1: - literal: '1' - pin__total_localtax2: - literal: '1' - pin__tva_tx: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_llx_facturedet|a row beside the main record holds lines - match: - main__customer: - param: customer - main__town: - param: town - p__lines_1_product_ref: - param: lines.1.product_ref - p__lines_1_qty: - literal: '1' - pin__buy_price_ht: - literal: '1' - pin__date_end: - literal: '1' - pin__date_start: - literal: '1' - pin__fk_code_ventilation: - literal: '1' - pin__fk_contract_line: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_parent_line: - literal: '1' - pin__fk_prev_id: - literal: '1' - pin__fk_product_fournisseur_price: - literal: '1' - pin__fk_remise_except: - literal: '1' - pin__fk_unit: - literal: '1' - pin__fk_user_author: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_warehouse: - literal: '1' - pin__info_bits: - literal: '1' - pin__localtax1_tx: - literal: '1' - localtax1_type: - literal: '0' - pin__localtax2_tx: - literal: '1' - localtax2_type: - literal: '0' - pin__multicurrency_code: - literal: '1' - pin__multicurrency_subprice: - literal: '1' - pin__multicurrency_subprice_ttc: - literal: '1' - pin__price: - literal: '1' - pin__product_type: - literal: '1' - pin__rang: - literal: '1' - pin__remise: - literal: '1' - pin__remise_percent: - literal: '1' - pin__situation_percent: - literal: '1' - pin__special_code: - literal: '1' - pin__subprice: - literal: '1' - pin__subprice_ttc: - literal: '1' - pin__total_localtax1: - literal: '1' - pin__total_localtax2: - literal: '1' - pin__tva_tx: - literal: '1' - expected_count: 1 - count_new_only: true -frame: - ignore_fields: - - fk_user_modif - - tms - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_facture - mode: skip - reason: read in full, every column, by this contract's own surface - - table: llx_facturedet - mode: skip - reason: read in full, every column, by this contract's own surface - views: {} - settle_s: 1 - settle_attempts: 5 - ignore_columns: - llx_societe: - - fk_user_modif - - socialnetworks - - tms - recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml deleted file mode 100644 index 108cdf3e93..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+town+linesx2.c91f9518.found.yaml +++ /dev/null @@ -1,88 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, found, fields customer, town, linesx2. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` - AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, - CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` - AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, - CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` - AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, - CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` - AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) - AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` - AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) - AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS - `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, - CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, - CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, - CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, - CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, - CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, - CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` - AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` - AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, - CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS - `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS - `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) - AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` - AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` - AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, - CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS - `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, - CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, - CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, - CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) - AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, - CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` - AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, (SELECT CAST(a1.`town` - AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town` FROM `llx_facture` - t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__customer: - param: customer - p__town: - param: town - expected_count: 1 -frame: - ignore_fields: - - fk_user_modif - - tms - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_facture - mode: skip - reason: read in full, every column, by this contract's own surface - views: {} - settle_s: 1 - settle_attempts: 5 - ignore_columns: - llx_societe: - - fk_user_modif - - socialnetworks - - tms - recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml index 74239e91f1..32e4406879 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml @@ -104,10 +104,71 @@ verifier: IS NULL THEN '1' ELSE '0' END AS `pin__width`, CASE WHEN t.`width_units` IS NULL THEN '1' ELSE '0' END AS `pin__width_units` FROM `llx_product` t sql_query_params: - v0: + v0: &id002 param: price_excl_vat - v1: + v1: &id003 param: vat_rate + rows_llx_product_price: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`entity` + AS CHAR) AS `entity`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_product` AS CHAR) AS `fk_product`, + CAST(t.`date_price` AS CHAR) AS `date_price`, CAST(t.`price_level` AS CHAR) AS `price_level`, + CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, CAST(t.`price_min` + AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, CAST(t.`price_base_type` + AS CHAR) AS `price_base_type`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, CAST(t.`tva_tx` + AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, CAST(t.`localtax1_tx` + AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, CAST(t.`localtax2_tx` + AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`price_label` AS CHAR) AS `price_label`, CAST(t.`tosell` + AS CHAR) AS `tosell`, CAST(t.`price_by_qty` AS CHAR) AS `price_by_qty`, CAST(t.`fk_price_expression` + AS CHAR) AS `fk_price_expression`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_price` AS CHAR) + AS `multicurrency_price`, CAST(t.`multicurrency_price_ttc` AS CHAR) AS `multicurrency_price_ttc`, + (SELECT CAST(a1.`label` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) AS `main__label`, + CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT a1.`price` FROM `llx_product` + a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `main__price_excl_vat`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` + = t.`fk_product`) AS `main__ref`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND (SELECT a1.`tva_tx` FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v1)s + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__vat_rate`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP + '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `p__price_excl_vat`, CASE WHEN t.`entity` = (SELECT s.`entity` FROM `llx_product` s WHERE s.`rowid` + = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fk_multicurrency` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_price_expression` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_price_expression`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` + FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, + CASE WHEN t.`localtax1_tx` = (SELECT s.`localtax1_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax1_type` = (SELECT s.`localtax1_type` + FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__localtax1_type`, + CASE WHEN t.`localtax2_tx` = (SELECT s.`localtax2_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`localtax2_type` = (SELECT s.`localtax2_type` + FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__localtax2_type`, + CASE WHEN t.`multicurrency_price` IS NULL THEN '1' ELSE '0' END AS `pin__multicurrency_price`, + CASE WHEN t.`multicurrency_price_ttc` IS NULL THEN '1' ELSE '0' END AS `pin__multicurrency_price_ttc`, + CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, + CASE WHEN t.`price_base_type` = (SELECT s.`price_base_type` FROM `llx_product` s WHERE s.`rowid` + = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__price_base_type`, CASE WHEN t.`price_by_qty` + = (SELECT s.`price_by_qty` FROM `llx_product_price` s WHERE s.`rowid` = t.`fk_user_author`) THEN + '1' ELSE '0' END AS `pin__price_by_qty`, CASE WHEN t.`price_level` = (SELECT s.`price_level` FROM + `llx_product_price` s WHERE s.`rowid` = t.`fk_user_author`) THEN '1' ELSE '0' END AS `pin__price_level`, + CASE WHEN t.`price_min` = (SELECT s.`price_min` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__price_min`, CASE WHEN t.`price_min_ttc` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__price_min_ttc`, CASE WHEN t.`recuperableonly` = (SELECT s.`recuperableonly` + FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__recuperableonly`, + CASE WHEN t.`tosell` = (SELECT s.`tosell` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) + THEN '1' ELSE '0' END AS `pin__tosell`, CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` + s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx`, CASE WHEN CAST(%(v1)s + AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN + '1' ELSE '0' END AS `p__vat_rate`, CASE WHEN t.`price` = (SELECT s.`price` FROM `llx_product` + s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__price` FROM `llx_product_price` + t + sql_query_params: + v0: *id002 + v1: *id003 raw_llx_actioncomm: kind: sql sql_driver: pymysql @@ -145,27 +206,6 @@ verifier: AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` t - raw_llx_product_price: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`entity` - AS CHAR) AS `entity`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_product` AS CHAR) AS `fk_product`, - CAST(t.`date_price` AS CHAR) AS `date_price`, CAST(t.`price_level` AS CHAR) AS `price_level`, - CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, CAST(t.`price_min` - AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, CAST(t.`price_base_type` - AS CHAR) AS `price_base_type`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, CAST(t.`tva_tx` - AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, CAST(t.`localtax1_tx` - AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, CAST(t.`localtax2_tx` - AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, CAST(t.`fk_user_author` - AS CHAR) AS `fk_user_author`, CAST(t.`price_label` AS CHAR) AS `price_label`, CAST(t.`tosell` - AS CHAR) AS `tosell`, CAST(t.`price_by_qty` AS CHAR) AS `price_by_qty`, CAST(t.`fk_price_expression` - AS CHAR) AS `fk_price_expression`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_price` AS CHAR) - AS `multicurrency_price`, CAST(t.`multicurrency_price_ttc` AS CHAR) AS `multicurrency_price_ttc` - FROM `llx_product_price` t effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -288,13 +328,115 @@ effects: literal: '1' expected_count: 1 count_new_only: true +- kind: record_written + probe: surface=rows_llx_product_price|a row beside the main record holds price_excl_vat + match: + main__label: + param: label + main__price_excl_vat: + literal: '1' + main__ref: + param: ref + main__vat_rate: + literal: '1' + p__price_excl_vat: + literal: '1' + pin__entity: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_price_expression: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__localtax1_tx: + literal: '1' + pin__localtax1_type: + literal: '1' + pin__localtax2_tx: + literal: '1' + pin__localtax2_type: + literal: '1' + pin__multicurrency_price: + literal: '1' + pin__multicurrency_price_ttc: + literal: '1' + pin__multicurrency_tx: + literal: '1' + pin__price_base_type: + literal: '1' + pin__price_by_qty: + literal: '1' + pin__price_level: + literal: '1' + pin__price_min: + literal: '1' + pin__price_min_ttc: + literal: '1' + pin__recuperableonly: + literal: '1' + pin__tosell: + literal: '1' + pin__tva_tx: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_llx_product_price|a row beside the main record holds vat_rate + match: + main__label: + param: label + main__price_excl_vat: + literal: '1' + main__ref: + param: ref + main__vat_rate: + literal: '1' + p__vat_rate: + literal: '1' + pin__entity: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_price_expression: + literal: '1' + pin__fk_user_author: + literal: '1' + pin__localtax1_tx: + literal: '1' + pin__localtax1_type: + literal: '1' + pin__localtax2_tx: + literal: '1' + pin__localtax2_type: + literal: '1' + pin__multicurrency_price: + literal: '1' + pin__multicurrency_price_ttc: + literal: '1' + pin__multicurrency_tx: + literal: '1' + pin__price: + literal: '1' + pin__price_base_type: + literal: '1' + pin__price_by_qty: + literal: '1' + pin__price_level: + literal: '1' + pin__price_min: + literal: '1' + pin__price_min_ttc: + literal: '1' + pin__recuperableonly: + literal: '1' + pin__tosell: + literal: '1' + expected_count: 1 + count_new_only: true - kind: record_written probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm match: - code: - literal: AC_PRODUCT_CREATE - elementtype: - literal: product entity: literal: '1' event_paid: @@ -332,13 +474,6 @@ effects: literal: '0' expected_count: 1 count_new_only: true -- kind: record_written - probe: surface=raw_llx_product_price|1 row(s) the app adds to llx_product_price - match: - fk_user_author: - param: bot_user_id - expected_count: 1 - count_new_only: true frame: ignore_fields: - fk_user_modif @@ -352,20 +487,20 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_product - mode: skip - reason: read in full, every column, by this contract's own surface + - table: llx_product_price views: llx_actioncomm: raw_llx_actioncomm llx_actioncomm_resources: raw_llx_actioncomm_resources - llx_product_price: raw_llx_product_price settle_s: 1 settle_attempts: 5 new_row_fields: llx_actioncomm: + - code - datec - datep - datep2 - durationp + - elementtype - email_from - email_msgid - email_sender @@ -393,31 +528,6 @@ frame: - fk_actioncomm - rowid - transparency - llx_product_price: - - date_price - - default_vat_code - - entity - - fk_product - - import_key - - localtax1_tx - - localtax1_type - - localtax2_tx - - localtax2_type - - multicurrency_code - - multicurrency_tx - - price - - price_base_type - - price_by_qty - - price_label - - price_level - - price_min - - price_min_ttc - - price_ttc - - recuperableonly - - rowid - - tms - - tosell - - tva_tx ignore_columns: llx_societe: - fk_user_modif diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml index 9a555fc8e7..3be453e4a0 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml @@ -61,10 +61,44 @@ verifier: AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate` FROM `llx_product` t sql_query_params: - v0: + v0: &id002 param: price_excl_vat - v1: + v1: &id003 param: vat_rate + rows_llx_product_price: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`entity` + AS CHAR) AS `entity`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_product` AS CHAR) AS `fk_product`, + CAST(t.`date_price` AS CHAR) AS `date_price`, CAST(t.`price_level` AS CHAR) AS `price_level`, + CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, CAST(t.`price_min` + AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, CAST(t.`price_base_type` + AS CHAR) AS `price_base_type`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, CAST(t.`tva_tx` + AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, CAST(t.`localtax1_tx` + AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, CAST(t.`localtax2_tx` + AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, CAST(t.`fk_user_author` + AS CHAR) AS `fk_user_author`, CAST(t.`price_label` AS CHAR) AS `price_label`, CAST(t.`tosell` + AS CHAR) AS `tosell`, CAST(t.`price_by_qty` AS CHAR) AS `price_by_qty`, CAST(t.`fk_price_expression` + AS CHAR) AS `fk_price_expression`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_multicurrency` + AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, + CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_price` AS CHAR) + AS `multicurrency_price`, CAST(t.`multicurrency_price_ttc` AS CHAR) AS `multicurrency_price_ttc`, + (SELECT CAST(a1.`label` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) AS `main__label`, + CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT a1.`price` FROM `llx_product` + a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `main__price_excl_vat`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` + = t.`fk_product`) AS `main__ref`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND (SELECT a1.`tva_tx` FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v1)s + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__vat_rate`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP + '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `p__price_excl_vat`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`tva_tx` + = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate` FROM `llx_product_price` + t + sql_query_params: + v0: *id002 + v1: *id003 effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -78,6 +112,34 @@ effects: p__vat_rate: literal: '1' expected_count: 1 +- kind: record_written + probe: surface=rows_llx_product_price|a row beside the main record holds price_excl_vat + match: + main__label: + param: label + main__price_excl_vat: + literal: '1' + main__ref: + param: ref + main__vat_rate: + literal: '1' + p__price_excl_vat: + literal: '1' + expected_count: 1 +- kind: record_written + probe: surface=rows_llx_product_price|a row beside the main record holds vat_rate + match: + main__label: + param: label + main__price_excl_vat: + literal: '1' + main__ref: + param: ref + main__vat_rate: + literal: '1' + p__vat_rate: + literal: '1' + expected_count: 1 frame: ignore_fields: - fk_user_modif @@ -91,8 +153,7 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_product - mode: skip - reason: read in full, every column, by this contract's own surface + - table: llx_product_price views: {} settle_s: 1 settle_attempts: 5 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml deleted file mode 100644 index 0536988a03..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml +++ /dev/null @@ -1,101 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind update_customer, found, fields customer, new_address, new_email, new_phone, town. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` - AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` - AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, - CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) - AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, - CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS - CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, - CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) - AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS - `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) - AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS - `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` - AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, - CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, - CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, - CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` - AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` - AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS - `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, - CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, - CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, - CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` - AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` - AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` - AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` - AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` - AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` - AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` - AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` - AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, - CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` - AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` - AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` - AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` - AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` - AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` - AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` - AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` - AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) - AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) - AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` - AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, - CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS - CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` - t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - nom: - param: customer - address: - param: new_address - email: - param: new_email - phone: - param: new_phone - kind: phone - town: - param: town - expected_count: 1 -frame: - ignore_fields: - - fk_user_modif - - socialnetworks - - tms - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_societe - mode: skip - reason: read in full, every column, by this contract's own surface - views: {} - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml deleted file mode 100644 index 788c0c1431..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml +++ /dev/null @@ -1,218 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind update_customer, update, fields customer, new_address, new_email, new_phone, town. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` - AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` - AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, - CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) - AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, - CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS - CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, - CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) - AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS - `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) - AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS - `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` - AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, - CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, - CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, - CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` - AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` - AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS - `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, - CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, - CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, - CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` - AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` - AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` - AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` - AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` - AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` - AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` - AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` - AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, - CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` - AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` - AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` - AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` - AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` - AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` - AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` - AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` - AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) - AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) - AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` - AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, - CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS - CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` - t - raw_llx_actioncomm: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` - AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, - CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` - AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` - AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` - AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS - `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, - CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, - CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` - AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) - AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, - CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` - AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` - AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` - AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS - CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) - AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, - CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` - AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS - `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` - AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` - AS CHAR) AS `extraparams` FROM `llx_actioncomm` t - raw_llx_actioncomm_resources: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` - AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` - AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` - AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` - t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - nom: - param: customer - address: - param: new_address - email: - param: new_email - phone: - param: new_phone - kind: phone - town: - param: town - expected_count: 1 -- kind: record_written - probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm - match: - entity: - literal: '1' - event_paid: - literal: '0' - fk_action: - literal: '40' - fk_parent: - literal: '0' - fk_user_action: - param: bot_user_id - fk_user_author: - param: bot_user_id - fulldayevent: - literal: '0' - percent: - literal: '-1' - priority: - literal: '0' - status: - literal: '0' - transparency: - literal: '0' - visibility: - literal: default - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources - match: - element_type: - literal: user - fk_element: - param: bot_user_id - mandatory: - literal: '0' - expected_count: 1 - count_new_only: true -frame: - ignore_fields: - - fk_user_modif - - socialnetworks - - tms - target_fields: - - address - - email - - phone - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_societe - mode: skip - reason: read in full, every column, by this contract's own surface - views: - llx_actioncomm: raw_llx_actioncomm - llx_actioncomm_resources: raw_llx_actioncomm_resources - settle_s: 1 - settle_attempts: 5 - new_row_fields: - llx_actioncomm: - - code - - datec - - datep - - datep2 - - durationp - - elementtype - - email_from - - email_msgid - - email_sender - - email_subject - - email_to - - email_tobcc - - email_tocc - - errors_to - - extraparams - - fk_soc - - id - - import_key - - ip - - label - - location - - note - - recurid - - recurrule - - ref - - ref_ext - - reply_to - - tms - llx_actioncomm_resources: - - answer_status - - fk_actioncomm - - rowid - - transparency - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml index 2d06c7591d..6b5b5cbc46 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml @@ -71,6 +71,7 @@ effects: param: customer email: param: new_email + kind: email phone: param: new_phone kind: phone @@ -91,8 +92,6 @@ frame: sql_password_env: FCG_DOLIBARR_READER_PASSWORD allow_tables: - table: llx_societe - mode: skip - reason: read in full, every column, by this contract's own surface views: {} settle_s: 1 settle_attempts: 5 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml index 6d51c78306..ac485d5716 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml @@ -108,6 +108,7 @@ effects: param: customer email: param: new_email + kind: email phone: param: new_phone kind: phone @@ -117,6 +118,8 @@ effects: - kind: record_written probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm match: + code: + literal: AC_COMPANY_MODIFY entity: literal: '1' event_paid: @@ -179,7 +182,6 @@ frame: settle_attempts: 5 new_row_fields: llx_actioncomm: - - code - datec - datep - datep2 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml index be7c828bd3..13fda668fb 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml @@ -32,6 +32,608 @@ kinds: dispatch_value: null create: contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml found: contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml + - fields: + - country + - email + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+phone+town+zip.53caf579.create.yaml + found: contracts/create_customer/country+email+name+phone+town+zip.53caf579.found.yaml + - fields: + - address + - email + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+phone+town+zip.e1f4b193.create.yaml + found: contracts/create_customer/address+email+name+phone+town+zip.e1f4b193.found.yaml + - fields: + - address + - country + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+phone+town+zip.f5e33735.create.yaml + found: contracts/create_customer/address+country+name+phone+town+zip.f5e33735.found.yaml + - fields: + - address + - country + - email + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+phone+town+zip.fbf8862c.create.yaml + found: contracts/create_customer/address+country+email+phone+town+zip.fbf8862c.found.yaml + - fields: + - address + - country + - email + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+town+zip.d1643b80.create.yaml + found: contracts/create_customer/address+country+email+name+town+zip.d1643b80.found.yaml + - fields: + - address + - country + - email + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+phone+zip.ccf08c47.create.yaml + found: contracts/create_customer/address+country+email+name+phone+zip.ccf08c47.found.yaml + - fields: + - address + - country + - email + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+phone+town.789a6442.create.yaml + found: contracts/create_customer/address+country+email+name+phone+town.789a6442.found.yaml + - fields: + - email + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/email+name+phone+town+zip.f154bfb3.create.yaml + found: contracts/create_customer/email+name+phone+town+zip.f154bfb3.found.yaml + - fields: + - country + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+name+phone+town+zip.0d88eb41.create.yaml + found: contracts/create_customer/country+name+phone+town+zip.0d88eb41.found.yaml + - fields: + - country + - email + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+phone+town+zip.4b6883d6.create.yaml + found: contracts/create_customer/country+email+phone+town+zip.4b6883d6.found.yaml + - fields: + - country + - email + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+town+zip.885cb135.create.yaml + found: contracts/create_customer/country+email+name+town+zip.885cb135.found.yaml + - fields: + - country + - email + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+phone+zip.fc167af2.create.yaml + found: contracts/create_customer/country+email+name+phone+zip.fc167af2.found.yaml + - fields: + - country + - email + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+phone+town.44f5a950.create.yaml + found: contracts/create_customer/country+email+name+phone+town.44f5a950.found.yaml + - fields: + - address + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+name+phone+town+zip.d9f5e809.create.yaml + found: contracts/create_customer/address+name+phone+town+zip.d9f5e809.found.yaml + - fields: + - address + - email + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+phone+town+zip.9a3bf199.create.yaml + found: contracts/create_customer/address+email+phone+town+zip.9a3bf199.found.yaml + - fields: + - address + - email + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+town+zip.2c8b3188.create.yaml + found: contracts/create_customer/address+email+name+town+zip.2c8b3188.found.yaml + - fields: + - address + - email + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+phone+zip.73ba341b.create.yaml + found: contracts/create_customer/address+email+name+phone+zip.73ba341b.found.yaml + - fields: + - address + - email + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+phone+town.fb28b6ed.create.yaml + found: contracts/create_customer/address+email+name+phone+town.fb28b6ed.found.yaml + - fields: + - address + - country + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+phone+town+zip.9e804046.create.yaml + found: contracts/create_customer/address+country+phone+town+zip.9e804046.found.yaml + - fields: + - address + - country + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+town+zip.a3c17aca.create.yaml + found: contracts/create_customer/address+country+name+town+zip.a3c17aca.found.yaml + - fields: + - address + - country + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+phone+zip.706c04dd.create.yaml + found: contracts/create_customer/address+country+name+phone+zip.706c04dd.found.yaml + - fields: + - address + - country + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+phone+town.5082b1a5.create.yaml + found: contracts/create_customer/address+country+name+phone+town.5082b1a5.found.yaml + - fields: + - address + - country + - email + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+town+zip.e2b6efe5.create.yaml + found: contracts/create_customer/address+country+email+town+zip.e2b6efe5.found.yaml + - fields: + - address + - country + - email + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+phone+zip.32467ede.create.yaml + found: contracts/create_customer/address+country+email+phone+zip.32467ede.found.yaml + - fields: + - address + - country + - email + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+phone+town.d258e0cb.create.yaml + found: contracts/create_customer/address+country+email+phone+town.d258e0cb.found.yaml + - fields: + - address + - country + - email + - name + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+zip.9c85e2ad.create.yaml + found: contracts/create_customer/address+country+email+name+zip.9c85e2ad.found.yaml + - fields: + - address + - country + - email + - name + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+town.7d754069.create.yaml + found: contracts/create_customer/address+country+email+name+town.7d754069.found.yaml + - fields: + - address + - country + - email + - name + - phone + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name+phone.a2b4ef09.create.yaml + found: contracts/create_customer/address+country+email+name+phone.a2b4ef09.found.yaml + - fields: + - name + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/name+phone+town+zip.fbceaff4.create.yaml + found: contracts/create_customer/name+phone+town+zip.fbceaff4.found.yaml + - fields: + - email + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/email+phone+town+zip.3535f506.create.yaml + found: contracts/create_customer/email+phone+town+zip.3535f506.found.yaml + - fields: + - email + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/email+name+town+zip.475b04b2.create.yaml + found: contracts/create_customer/email+name+town+zip.475b04b2.found.yaml + - fields: + - email + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/email+name+phone+zip.d936d504.create.yaml + found: contracts/create_customer/email+name+phone+zip.d936d504.found.yaml + - fields: + - email + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/email+name+phone+town.5328a595.create.yaml + found: contracts/create_customer/email+name+phone+town.5328a595.found.yaml + - fields: + - country + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+phone+town+zip.ac1af0a2.create.yaml + found: contracts/create_customer/country+phone+town+zip.ac1af0a2.found.yaml + - fields: + - country + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+name+town+zip.4a8420e2.create.yaml + found: contracts/create_customer/country+name+town+zip.4a8420e2.found.yaml + - fields: + - country + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+name+phone+zip.a7ae9bc0.create.yaml + found: contracts/create_customer/country+name+phone+zip.a7ae9bc0.found.yaml + - fields: + - country + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/country+name+phone+town.9c66266e.create.yaml + found: contracts/create_customer/country+name+phone+town.9c66266e.found.yaml + - fields: + - country + - email + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+town+zip.202dbae5.create.yaml + found: contracts/create_customer/country+email+town+zip.202dbae5.found.yaml + - fields: + - country + - email + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+phone+zip.23211a11.create.yaml + found: contracts/create_customer/country+email+phone+zip.23211a11.found.yaml + - fields: + - country + - email + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+phone+town.160469ad.create.yaml + found: contracts/create_customer/country+email+phone+town.160469ad.found.yaml + - fields: + - country + - email + - name + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+zip.db502169.create.yaml + found: contracts/create_customer/country+email+name+zip.db502169.found.yaml + - fields: + - country + - email + - name + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+town.bc62857c.create.yaml + found: contracts/create_customer/country+email+name+town.bc62857c.found.yaml + - fields: + - country + - email + - name + - phone + lists: {} + dispatch_value: null + create: contracts/create_customer/country+email+name+phone.92f6dc3e.create.yaml + found: contracts/create_customer/country+email+name+phone.92f6dc3e.found.yaml + - fields: + - address + - phone + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+phone+town+zip.476dcfe8.create.yaml + found: contracts/create_customer/address+phone+town+zip.476dcfe8.found.yaml + - fields: + - address + - name + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+name+town+zip.3c79c185.create.yaml + found: contracts/create_customer/address+name+town+zip.3c79c185.found.yaml + - fields: + - address + - name + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+name+phone+zip.ee0d62a0.create.yaml + found: contracts/create_customer/address+name+phone+zip.ee0d62a0.found.yaml + - fields: + - address + - name + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+name+phone+town.c22c2f3c.create.yaml + found: contracts/create_customer/address+name+phone+town.c22c2f3c.found.yaml + - fields: + - address + - email + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+town+zip.73b7baf0.create.yaml + found: contracts/create_customer/address+email+town+zip.73b7baf0.found.yaml + - fields: + - address + - email + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+phone+zip.ea4966f9.create.yaml + found: contracts/create_customer/address+email+phone+zip.ea4966f9.found.yaml + - fields: + - address + - email + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+phone+town.0dfd0d0a.create.yaml + found: contracts/create_customer/address+email+phone+town.0dfd0d0a.found.yaml + - fields: + - address + - email + - name + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+zip.6b73f20c.create.yaml + found: contracts/create_customer/address+email+name+zip.6b73f20c.found.yaml + - fields: + - address + - email + - name + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+town.59ece868.create.yaml + found: contracts/create_customer/address+email+name+town.59ece868.found.yaml + - fields: + - address + - email + - name + - phone + lists: {} + dispatch_value: null + create: contracts/create_customer/address+email+name+phone.c37efe1f.create.yaml + found: contracts/create_customer/address+email+name+phone.c37efe1f.found.yaml + - fields: + - address + - country + - town + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+town+zip.13199686.create.yaml + found: contracts/create_customer/address+country+town+zip.13199686.found.yaml + - fields: + - address + - country + - phone + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+phone+zip.5e48d952.create.yaml + found: contracts/create_customer/address+country+phone+zip.5e48d952.found.yaml + - fields: + - address + - country + - phone + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+phone+town.5d9fa0bc.create.yaml + found: contracts/create_customer/address+country+phone+town.5d9fa0bc.found.yaml + - fields: + - address + - country + - name + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+zip.0c72fe0a.create.yaml + found: contracts/create_customer/address+country+name+zip.0c72fe0a.found.yaml + - fields: + - address + - country + - name + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+town.0d9093f0.create.yaml + found: contracts/create_customer/address+country+name+town.0d9093f0.found.yaml + - fields: + - address + - country + - name + - phone + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+name+phone.9cb52e23.create.yaml + found: contracts/create_customer/address+country+name+phone.9cb52e23.found.yaml + - fields: + - address + - country + - email + - zip + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+zip.df797a7c.create.yaml + found: contracts/create_customer/address+country+email+zip.df797a7c.found.yaml + - fields: + - address + - country + - email + - town + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+town.dd75ab46.create.yaml + found: contracts/create_customer/address+country+email+town.dd75ab46.found.yaml + - fields: + - address + - country + - email + - phone + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+phone.fa31e92e.create.yaml + found: contracts/create_customer/address+country+email+phone.fa31e92e.found.yaml + - fields: + - address + - country + - email + - name + lists: {} + dispatch_value: null + create: contracts/create_customer/address+country+email+name.736afe34.create.yaml + found: contracts/create_customer/address+country+email+name.736afe34.found.yaml create_product: mode: create main_table: llx_product @@ -53,6 +655,29 @@ kinds: dispatch_value: null create: contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml found: contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml + - fields: + - label + - ref + - vat_rate + lists: {} + dispatch_value: null + create: contracts/create_product/label+ref+vat_rate.c78fc3bd.create.yaml + found: contracts/create_product/label+ref+vat_rate.c78fc3bd.found.yaml + - fields: + - label + - price_excl_vat + - ref + lists: {} + dispatch_value: null + create: contracts/create_product/label+price_excl_vat+ref.0f575d09.create.yaml + found: contracts/create_product/label+price_excl_vat+ref.0f575d09.found.yaml + - fields: + - label + - ref + lists: {} + dispatch_value: null + create: contracts/create_product/label+ref.153a9f2e.create.yaml + found: contracts/create_product/label+ref.153a9f2e.found.yaml create_draft_invoice: mode: create main_table: llx_facture diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json index be5ce10a31..916e1bc81e 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json @@ -6,7 +6,7 @@ { "number": 1, "key": "bookkeeping", - "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", + "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): llx_actioncomm (1 per write), llx_actioncomm_resources (1 per write). Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", "proposal": "accept", "answered": true }, @@ -32,15 +32,17 @@ "locations": { "address": "llx_societe.address (exact)", "country": "llx_societe.fk_pays -> llx_c_country.label (exact)", - "email": "llx_societe.email (exact)", + "email": "llx_societe.email (email)", "name": "llx_societe.nom (exact)", "phone": "llx_societe.phone (phone)", "town": "llx_societe.town (exact)", "zip": "llx_societe.zip (exact)" }, "placed_by_name": [], + "placed_from_another_kind": [], "demonstrations": 2, "children": {}, + "dispatch_children": {}, "lists": {}, "side_rows": [ { @@ -57,7 +59,7 @@ "logs": [], "refused": [], "unresolved": {}, - "emitted": 1, + "emitted": 64, "rejected": [] }, "create_product": { @@ -70,8 +72,13 @@ "vat_rate": "llx_product.tva_tx (number)" }, "placed_by_name": [], + "placed_from_another_kind": [], "demonstrations": 2, - "children": {}, + "children": { + "price_excl_vat#2": "llx_product_price", + "vat_rate#2": "llx_product_price" + }, + "dispatch_children": {}, "lists": {}, "side_rows": [ { @@ -83,17 +90,12 @@ "table": "llx_actioncomm_resources", "count": 1, "unless": null - }, - { - "table": "llx_product_price", - "count": 1, - "unless": null } ], "logs": [], "refused": [], "unresolved": {}, - "emitted": 1, + "emitted": 4, "rejected": [] }, "create_draft_invoice": { @@ -105,8 +107,10 @@ "town": "llx_facture.fk_soc -> llx_societe.town (exact)" }, "placed_by_name": [], + "placed_from_another_kind": [], "demonstrations": 2, "children": {}, + "dispatch_children": {}, "lists": { "lines": "llx_facturedet" }, @@ -123,7 +127,7 @@ "locations": { "customer": "llx_societe.nom (exact)", "new_address": "llx_societe.address (exact)", - "new_email": "llx_societe.email (exact)", + "new_email": "llx_societe.email (email)", "new_phone": "llx_societe.phone (phone)", "new_zip": "llx_societe.zip (exact)", "town": "llx_societe.town (exact)" @@ -132,8 +136,10 @@ "new_address", "new_zip" ], + "placed_from_another_kind": [], "demonstrations": 2, "children": {}, + "dispatch_children": {}, "lists": {}, "side_rows": [ { From c4409878ef99493a0ba1bdc8c87c2aef4a9215d6 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 17:42:28 -0400 Subject: [PATCH 68/94] WIP: frame.versions from a version column or the app's audit trail --- .../benchmark/generate_contracts/RESULTS.md | 95 +++++++++++ ...ject+subject+tracker.d268453f.create.yaml} | 63 ++++---- ...oject+subject+tracker.d268453f.found.yaml} | 153 +++++++++++------- ...ject+project+spent_on.a2ffb74d.create.yaml | 8 + ...ject+project+spent_on.a2ffb74d.found.yaml} | 22 +-- ...roject+status+subject.4aa4d777.found.yaml} | 116 +++++++------ .../results/redmine/manifest.yaml | 98 +++++------ .../results/redmine/report.json | 6 +- .../generate/verify_contract.py | 146 +++++++++++++++++ .../tests/test_generate_verify_contract.py | 4 + 10 files changed, 508 insertions(+), 203 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/RESULTS.md rename packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/{assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml => assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml} (94%) rename packages/flow/benchmark/generate_contracts/results/redmine/contracts/{log_time/activity+hours+project+spent_on.e07feee9.create.yaml => create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml} (89%) rename packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/{activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml => activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml} (98%) rename packages/flow/benchmark/generate_contracts/results/redmine/contracts/{log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml => update_issue/notes+project+status+subject.4aa4d777.found.yaml} (95%) diff --git a/packages/flow/benchmark/generate_contracts/RESULTS.md b/packages/flow/benchmark/generate_contracts/RESULTS.md new file mode 100644 index 0000000000..233ce40111 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/RESULTS.md @@ -0,0 +1,95 @@ +# Generated contracts on the first study's apps + +Written 2026-10-30. The generator (`python -m openadapt_flow.generate.verify_contract`) +was run on three of the first-contact study's apps, each on its own copy: +Dolibarr instance 1, Redmine instance 1 and EspoCRM instance 2. Each run used +only what a new customer has: + +- the app's read-only database account (the fixture's `read_access()`); +- the kinds and parameter names, and the example values, from the app's + `setup_view.yaml`; +- one demonstration write per example, made here through the app's public + REST API as the bot (`demo_.py`). An example whose record already + existed was replaced by a create with values of our own. + +The study's tasks, oracle and fault scripts were used only afterwards, by +`validate.py`, to judge the contracts: each task's reference correct write +and each planted fault, through `choose`, `begin_write` and `verify_write`. +Late-write faults also ran the recheck the contract declares. + +## Results + +| | Dolibarr | Redmine | EspoCRM | +| --- | --- | --- | --- | +| Database | MariaDB 11.4 | PostgreSQL | MariaDB | +| Questions for a person | **3** | **1** | **ESPO_Q** | +| First study, person decisions | 11 | 11 | 11 | +| Contracts written | 93 | 95 | ESPO_N | +| Correct writes confirmed (tasks and controls) | 40/43 | 35/38 | ESPO_C | +| In-scope planted faults stopped | **22/22** | **19/19** | **ESPO_I** | +| First study, in-scope faults stopped | 21/21 | 26/26 | 16/19 | +| Out-of-scope late writes stopped | 2/2 (recheck) | 0/2 (no recheck on PostgreSQL) | ESPO_O | +| `propose` time | 1.5 min | 2.5 min | ESPO_T | + +The first study measured false stops on agents' writes; here the correct +writes are the pack's reference writes and the controls, so the two false +stop rates are not the same measure. + +### Questions asked + +- **Dolibarr** (3): bookkeeping (agenda events per write, `tms`, + `fk_user_modif`, the `socialnetworks` rewrite, invoice totals and draft + reference not checked); the recheck window (300 s proposed and accepted); + `invoice_date` is in `datef` and in `date_lim_reglement` (picked `datef`). +- **Redmine** (1): bookkeeping (custom field rows per write, `journal_details` + as a log, `updated_on`, `lock_version`, `user_preferences.others`, and the + time entry's `tyear`, `tmonth`, `tweek`). No recheck question: PostgreSQL + has no whole-database frame. +- **EspoCRM** (ESPO_QN) + +Every answer was the proposal, except the one choice in Dolibarr's question 3. + +### Setup steps the generator applied + +- Dolibarr and EspoCRM: `GRANT TRIGGER ON .* TO ` + (`--grant-trigger`): the whole-database frame must see triggers. +- ESPO_SETUP + +### False stops, by cause + +| App | Case | Cause | +| --- | --- | --- | +| Dolibarr | F18 (control) | Another user's edit to an unrelated customer: no audit trail is read, so it isn't attributed | +| Dolibarr | F21 (control) | Two saves add two agenda events; the contract expects one per write | +| Dolibarr | F30 (control) | A resave of a value already in place adds an agenda event; the `found` contract allows no change | +| Redmine | T19 | Closing an issue sets `closed_on`; no demonstration closed one | +| Redmine | F21 (control) | Another user's edit to an unrelated issue: not attributed | +| Redmine | F24 (control) | Two saves add two journals | +ESPO_FS + +### Misses + +Out of scope only: Redmine F19 and F20 (late writes; PostgreSQL has no +recheck). ESPO_MISS No in-scope planted fault was confirmed on any app. + +## Reproduce + +```bash +cd packages/flow/benchmark/generate_contracts +FCG_INSTANCE=1 FCG_DOLIBARR_READER_PASSWORD=... python demo_dolibarr.py WORK +python -m openadapt_flow.generate.verify_contract propose --read WORK/read.yaml \ + --kinds WORK/kinds.yaml --demos WORK/demos --out WORK/out \ + --actor-param bot_user_id=2 --answers WORK/answers.yaml \ + --grant-trigger --admin-read admin.yaml --reader-account "'fcg_reader'@'%'" +FCG_INSTANCE=1 python validate.py dolibarr WORK --actor-id 2 +python summarize.py dolibarr WORK +``` + +`results//` holds each run's `QUESTIONS.md`, `answers.yaml`, +`report.json`, `manifest.yaml` and two example contract pairs per app (the +full set is regenerated by the commands above). The fault scripts and the +oracle stay in the study's private pack. + +Code: Dolibarr and Redmine ran on this branch at `a6b5a3915` with #1661 (the +whole-database frame fix) merged in; EspoCRM ran after the rebase on `main` +with #1661, #1662, #1663, #1665, #1666, #1668 and #1669. diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml similarity index 94% rename from packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml rename to packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml index 9ffc2e8b23..8cebbfa15d 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml @@ -1,4 +1,4 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_issue, create, fields assignee, customer_ref, due_date, environment, priority, project, subject, tracker. +# Generated by openadapt_flow.generate.verify_contract: kind create_issue, create, fields assignee, customer_ref, description, environment, priority, project, subject, tracker. # Proposed, not admitted: a person answers QUESTIONS.md first. schema_version: openadapt.verify/v1 environment_id: generated-redmine @@ -27,9 +27,8 @@ verifier: TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."firstname" AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) FROM "users" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" - AS TEXT)) AS "p__assignee", CASE WHEN t."due_date" = CAST(%(v0)s AS date) THEN '1' ELSE '0' END - AS "p__due_date", (SELECT CAST(a1."name" AS TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS - TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CAST(a1."name" AS TEXT) FROM + AS TEXT)) AS "p__assignee", (SELECT CAST(a1."name" AS TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT CAST(a1."name" AS TEXT) FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" AS TEXT)) AS "p__tracker", CASE WHEN t."category_id" IS NULL THEN '1' ELSE '0' END AS "pin__category_id", @@ -43,11 +42,8 @@ verifier: IS NULL THEN '1' ELSE '0' END AS "pin__parent_id", CASE WHEN CAST(t."rgt" AS numeric) = CAST('2' AS numeric) THEN '1' ELSE '0' END AS "pin__rgt", CASE WHEN t."start_date" IS NULL THEN '1' ELSE '0' END AS "pin__start_date", CASE WHEN CAST(t."status_id" AS numeric) = CAST('1' AS numeric) - THEN '1' ELSE '0' END AS "pin__status_id", CASE WHEN t."description" IS NULL OR t."description" - = '' THEN '1' ELSE '0' END AS "empty__description" FROM "issues" t - sql_query_params: - v0: &id002 - param: due_date + THEN '1' ELSE '0' END AS "pin__status_id", CASE WHEN t."due_date" IS NULL THEN '1' ELSE '0' END + AS "empty__due_date" FROM "issues" t rows_custom_values: kind: sql sql_driver: psycopg2 @@ -57,20 +53,18 @@ verifier: CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CAST(a2."firstname" AS TEXT) || ' ' || CAST(a2."lastname" AS TEXT) FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" AS TEXT)) FROM - "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", CASE WHEN (SELECT a1."due_date" - FROM "issues" a1 WHERE a1."id" = t."customized_id") = CAST(%(v0)s AS date) THEN '1' ELSE '0' END - AS "main__due_date", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "enumerations" a2 WHERE CAST(a2."id" - AS TEXT) = CAST(a1."priority_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") - AS "main__priority", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE CAST(a2."id" - AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") - AS "main__project", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") - AS "main__subject", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "trackers" a2 WHERE CAST(a2."id" - AS TEXT) = CAST(a1."tracker_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") - AS "main__tracker", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('1' AS numeric) THEN - '1' ELSE '0' END AS "pin__custom_field_id", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('2' - AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id__47c66b" FROM "custom_values" t - sql_query_params: - v0: *id002 + "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", (SELECT CAST(a1."description" + AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__description", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "enumerations" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."priority_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__priority", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__project", (SELECT CAST(a1."subject" + AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__subject", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "trackers" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."tracker_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__tracker", CASE WHEN CAST(t."custom_field_id" + AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id", CASE WHEN + CAST(t."custom_field_id" AS numeric) = CAST('2' AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id__47c66b" + FROM "custom_values" t raw_ar_internal_metadata: kind: sql sql_driver: psycopg2 @@ -283,6 +277,14 @@ verifier: AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" t + raw_journal_details: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" + AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) + AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t raw_journals: kind: sql sql_driver: psycopg2 @@ -580,8 +582,8 @@ effects: match: p__assignee: param: assignee - p__due_date: - literal: '1' + description: + param: description p__priority: param: priority p__project: @@ -616,7 +618,7 @@ effects: literal: '1' pin__status_id: literal: '1' - empty__description: + empty__due_date: literal: '1' expected_count: 1 count_new_only: true @@ -625,8 +627,8 @@ effects: match: main__assignee: param: assignee - main__due_date: - literal: '1' + main__description: + param: description main__priority: param: priority main__project: @@ -637,6 +639,7 @@ effects: param: tracker value: param: customer_ref + kind: phone pin__custom_field_id: literal: '1' customized_type: @@ -648,8 +651,8 @@ effects: match: main__assignee: param: assignee - main__due_date: - literal: '1' + main__description: + param: description main__priority: param: priority main__project: diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml similarity index 89% rename from packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml rename to packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml index 80afe1cb97..9802b204ef 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml @@ -1,4 +1,4 @@ -# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, hours, project, spent_on. +# Generated by openadapt_flow.generate.verify_contract: kind create_issue, found, fields assignee, customer_ref, description, environment, priority, project, subject, tracker. # Proposed, not admitted: a person answers QUESTIONS.md first. schema_version: openadapt.verify/v1 environment_id: generated-redmine @@ -15,24 +15,42 @@ verifier: port: 9606 user: oa_reader sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", - CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" - AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", - CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS - TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", - CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", - (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" - AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END - AS "p__spent_on", CASE WHEN t."comments" IS NULL OR t."comments" = '' THEN '1' ELSE '0' END AS - "empty__comments", CASE WHEN t."issue_id" IS NULL THEN '1' ELSE '0' END AS "empty__issue_id" FROM - "time_entries" t - sql_query_params: - v0: - param: hours - v1: - param: spent_on + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."firstname" + AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) FROM "users" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" + AS TEXT)) AS "p__assignee", (SELECT CAST(a1."name" AS TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CAST(a1."name" AS TEXT) FROM + "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT + CAST(a1."name" AS TEXT) FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" AS + TEXT)) AS "p__tracker" FROM "issues" t + rows_custom_values: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", + CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", + CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CAST(a2."firstname" AS TEXT) || ' ' || CAST(a2."lastname" + AS TEXT) FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" AS TEXT)) FROM + "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", (SELECT CAST(a1."description" + AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__description", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "enumerations" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."priority_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__priority", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__project", (SELECT CAST(a1."subject" + AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__subject", (SELECT (SELECT + CAST(a2."name" AS TEXT) FROM "trackers" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."tracker_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__tracker" FROM "custom_values" + t raw_ar_internal_metadata: kind: sql sql_driver: psycopg2 @@ -161,14 +179,6 @@ verifier: sql_password_env: REDMINE_RO_DB_PASSWORD sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS TEXT) AS "tracker_id" FROM "custom_fields_trackers" t - raw_custom_values: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", - CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t raw_documents: kind: sql sql_driver: psycopg2 @@ -253,22 +263,14 @@ verifier: AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" t - raw_issues: + raw_journal_details: kind: sql sql_driver: psycopg2 sql_connect_args: *id001 sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) - AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", - CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", - CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", - CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", - CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" - AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" - AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS - TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" + AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) + AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t raw_journals: kind: sql sql_driver: psycopg2 @@ -437,6 +439,17 @@ verifier: sql_password_env: REDMINE_RO_DB_PASSWORD sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_time_entries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t raw_tokens: kind: sql sql_driver: psycopg2 @@ -553,24 +566,56 @@ effects: - kind: record_written probe: surface=record|the main record holds the task's values match: - p__activity: - param: activity - p__hours: - literal: '1' + p__assignee: + param: assignee + description: + param: description + p__priority: + param: priority p__project: param: project - p__spent_on: - literal: '1' - author_id: - param: bot_user_id - user_id: - param: bot_user_id - empty__comments: - literal: '1' - empty__issue_id: - literal: '1' + subject: + param: subject + p__tracker: + param: tracker + expected_count: 1 +- kind: record_written + probe: surface=rows_custom_values|a row beside the main record holds customer_ref + match: + main__assignee: + param: assignee + main__description: + param: description + main__priority: + param: priority + main__project: + param: project + main__subject: + param: subject + main__tracker: + param: tracker + value: + param: customer_ref + kind: phone + expected_count: 1 +- kind: record_written + probe: surface=rows_custom_values|a row beside the main record holds environment + match: + main__assignee: + param: assignee + main__description: + param: description + main__priority: + param: priority + main__project: + param: project + main__subject: + param: subject + main__tracker: + param: tracker + value: + param: environment expected_count: 1 - count_new_only: true frame: ignore_fields: - lock_version diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml index 0de24b0cd5..a8e88de096 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml @@ -268,6 +268,14 @@ verifier: AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journal_details: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" + AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) + AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t raw_journals: kind: sql sql_driver: psycopg2 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml similarity index 98% rename from packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml rename to packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml index e1d5b82da8..93e2caefcc 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml @@ -1,4 +1,4 @@ -# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, hours, issue_subject, project, spent_on. +# Generated by openadapt_flow.generate.verify_contract: kind log_time, found, fields activity, comments, hours, issue_subject, project, spent_on. # Proposed, not admitted: a person answers QUESTIONS.md first. schema_version: openadapt.verify/v1 environment_id: generated-redmine @@ -26,8 +26,7 @@ verifier: (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."issue_id" AS TEXT)) AS "p__issue_subject", (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s - AS date) THEN '1' ELSE '0' END AS "p__spent_on", CASE WHEN t."comments" IS NULL OR t."comments" - = '' THEN '1' ELSE '0' END AS "empty__comments" FROM "time_entries" t + AS date) THEN '1' ELSE '0' END AS "p__spent_on" FROM "time_entries" t sql_query_params: v0: param: hours @@ -269,6 +268,14 @@ verifier: AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t + raw_journal_details: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" + AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) + AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t raw_journals: kind: sql sql_driver: psycopg2 @@ -555,6 +562,8 @@ effects: match: p__activity: param: activity + comments: + param: comments p__hours: literal: '1' p__issue_subject: @@ -563,14 +572,7 @@ effects: param: project p__spent_on: literal: '1' - author_id: - param: bot_user_id - user_id: - param: bot_user_id - empty__comments: - literal: '1' expected_count: 1 - count_new_only: true frame: ignore_fields: - lock_version diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml similarity index 95% rename from packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml rename to packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml index edbd5dd64f..fc5336b4d8 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml @@ -1,4 +1,4 @@ -# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, comments, hours, project, spent_on. +# Generated by openadapt_flow.generate.verify_contract: kind update_issue, found, fields notes, project, status, subject. # Proposed, not admitted: a person answers QUESTIONS.md first. schema_version: openadapt.verify/v1 environment_id: generated-redmine @@ -15,23 +15,35 @@ verifier: port: 9606 user: oa_reader sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", - CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" - AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", - CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS - TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", - CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", - (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" - AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END - AS "p__spent_on", CASE WHEN t."issue_id" IS NULL THEN '1' ELSE '0' END AS "empty__issue_id" FROM - "time_entries" t - sql_query_params: - v0: - param: hours - v1: - param: spent_on + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" + AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) + AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", + CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", + CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", + CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", + CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" + AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" + AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS + TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."name" AS + TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", + (SELECT CAST(a1."name" AS TEXT) FROM "issue_statuses" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."status_id" + AS TEXT)) AS "p__status" FROM "issues" t + rows_journals: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", + CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", + CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" + AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" + AS TEXT) AS "updated_by_id", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE + CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__project", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "issue_statuses" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."status_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__status", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."journalized_id") + AS "main__subject" FROM "journals" t raw_ar_internal_metadata: kind: sql sql_driver: psycopg2 @@ -252,32 +264,6 @@ verifier: AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" t - raw_issues: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) - AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", - CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", - CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", - CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", - CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" - AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" - AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS - TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t - raw_journals: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", - CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" - AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" - AS TEXT) AS "updated_by_id" FROM "journals" t raw_member_roles: kind: sql sql_driver: psycopg2 @@ -436,6 +422,17 @@ verifier: sql_password_env: REDMINE_RO_DB_PASSWORD sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t + raw_time_entries: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" + AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", + CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" + AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", + CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t raw_tokens: kind: sql sql_driver: psycopg2 @@ -552,24 +549,25 @@ effects: - kind: record_written probe: surface=record|the main record holds the task's values match: - p__activity: - param: activity - comments: - param: comments - p__hours: - literal: '1' p__project: param: project - p__spent_on: - literal: '1' - author_id: - param: bot_user_id - user_id: - param: bot_user_id - empty__issue_id: - literal: '1' + p__status: + param: status + subject: + param: subject + expected_count: 1 +- kind: record_written + probe: surface=rows_journals|a row beside the main record holds notes + match: + main__project: + param: project + main__status: + param: status + main__subject: + param: subject + notes: + param: notes expected_count: 1 - count_new_only: true frame: ignore_fields: - lock_version diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml index aca034aa1e..6249f3d711 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml @@ -33,47 +33,38 @@ kinds: dispatch_value: null create: contracts/create_issue/assignee+due_date+priority+project+subject+tracker.cb8524d8.create.yaml found: contracts/create_issue/assignee+due_date+priority+project+subject+tracker.cb8524d8.found.yaml - - fields: - - due_date - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.create.yaml - found: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.found.yaml - fields: - assignee + - customer_ref + - environment - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.create.yaml - found: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.found.yaml + create: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.create.yaml + found: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.found.yaml - fields: + - due_date - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/priority+project+subject+tracker.a0dc8790.create.yaml - found: contracts/create_issue/priority+project+subject+tracker.a0dc8790.found.yaml + create: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.create.yaml + found: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.found.yaml - fields: - assignee - - customer_ref - - environment - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.create.yaml - found: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.found.yaml + create: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.create.yaml + found: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.found.yaml - fields: - customer_ref - environment @@ -107,6 +98,15 @@ kinds: dispatch_value: null create: contracts/create_issue/assignee+customer_ref+priority+project+subject+tracker.88f7523d.create.yaml found: contracts/create_issue/assignee+customer_ref+priority+project+subject+tracker.88f7523d.found.yaml + - fields: + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/priority+project+subject+tracker.a0dc8790.create.yaml + found: contracts/create_issue/priority+project+subject+tracker.a0dc8790.found.yaml - fields: - environment - priority @@ -164,28 +164,21 @@ kinds: create: contracts/create_issue/assignee+due_date+environment+priority+project+subject+tracker.9f2faa3a.create.yaml found: contracts/create_issue/assignee+due_date+environment+priority+project+subject+tracker.9f2faa3a.found.yaml - fields: + - assignee - customer_ref - - due_date - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.create.yaml - found: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.found.yaml - - fields: - description - - due_date + - environment - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.create.yaml - found: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.found.yaml + create: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml + found: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml - fields: + - assignee + - customer_ref - due_date - environment - priority @@ -194,33 +187,32 @@ kinds: - tracker lists: {} dispatch_value: null - create: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.create.yaml - found: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.found.yaml + create: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml + found: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.found.yaml - fields: - - assignee - - description + - customer_ref + - due_date - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.create.yaml - found: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.found.yaml + create: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.create.yaml + found: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.found.yaml - fields: - description + - due_date - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.create.yaml - found: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.found.yaml + create: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.create.yaml + found: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.found.yaml - fields: - - assignee - - customer_ref - - description + - due_date - environment - priority - project @@ -228,21 +220,19 @@ kinds: - tracker lists: {} dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml - found: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml + create: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.create.yaml + found: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.found.yaml - fields: - assignee - - customer_ref - - due_date - - environment + - description - priority - project - subject - tracker lists: {} dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml - found: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.found.yaml + create: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.create.yaml + found: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.found.yaml - fields: - customer_ref - description @@ -291,6 +281,16 @@ kinds: dispatch_value: null create: contracts/create_issue/assignee+customer_ref+description+priority+project+subject+tracker.4d3aeb69.create.yaml found: contracts/create_issue/assignee+customer_ref+description+priority+project+subject+tracker.4d3aeb69.found.yaml + - fields: + - description + - priority + - project + - subject + - tracker + lists: {} + dispatch_value: null + create: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.create.yaml + found: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.found.yaml - fields: - description - environment diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/report.json b/packages/flow/benchmark/generate_contracts/results/redmine/report.json index 76309b9659..188f7efd34 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/report.json +++ b/packages/flow/benchmark/generate_contracts/results/redmine/report.json @@ -17,7 +17,7 @@ "main_table": "issues", "locations": { "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", - "customer_ref": "custom_values (whose customized_id is the issues key).value (exact)", + "customer_ref": "custom_values (whose customized_id is the issues key).value (phone)", "description": "issues.description (exact)", "due_date": "issues.due_date (date)", "environment": "custom_values (whose customized_id is the issues key).value (exact)", @@ -35,6 +35,7 @@ "customer_ref": "custom_values", "environment": "custom_values" }, + "dispatch_children": {}, "lists": {}, "side_rows": [], "logs": [], @@ -67,6 +68,7 @@ "children": { "notes": "journals" }, + "dispatch_children": {}, "lists": {}, "side_rows": [], "logs": [ @@ -92,6 +94,7 @@ "placed_from_another_kind": [], "demonstrations": 2, "children": {}, + "dispatch_children": {}, "lists": {}, "side_rows": [], "logs": [], @@ -114,6 +117,7 @@ "placed_from_another_kind": [], "demonstrations": 2, "children": {}, + "dispatch_children": {}, "lists": {}, "side_rows": [ { diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 612ac09b51..43bd614da2 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -249,6 +249,11 @@ class KindPlan: by_name: list[str] = field(default_factory=list) #: Parameters placed where another kind's demonstrations found them. shared: list[str] = field(default_factory=list) + #: Where the target's versions are read (``frame.versions``): a version + #: column that steps by one per save, or an audit table naming the + #: record and the user who saved it. None when the demonstrations show + #: neither. + versions: Optional[dict[str, Any]] = None #: More places a value is in: rows the write added beside the record #: (a call's lead is its parent and also an attendee row). extra: dict[str, list[Location]] = field(default_factory=dict) @@ -894,6 +899,140 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N extra = plan.anchor_changed - explained if extra: plan.bookkeeping.setdefault(plan.anchor, set()).update(extra) + plan.versions = _audit_source(schema, plan, actor_values) or _version_column( + schema, plan + ) + + +_WHOLE_NUMBER = re.compile(r"^-?[0-9]+$") + + +def _version_column(schema: Schema, plan: KindPlan) -> Optional[dict[str, Any]]: + """A whole-number column of the main record that went up by exactly one + in every update demonstration (Redmine's ``lock_version``).""" + + t = schema.tables[plan.anchor] + if len(t.primary_key) != 1: + return None + for col in t.columns: + if col.kind != "number" or col.name in t.primary_key: + continue + ok = bool(plan.anchors) + for a in plan.anchors: + b0 = (a.before or {}).get(col.name) + b1 = (a.after or {}).get(col.name) + if not ( + b0 is not None + and b1 is not None + and _WHOLE_NUMBER.match(b0) + and _WHOLE_NUMBER.match(b1) + and int(b1) == int(b0) + 1 + ): + ok = False + break + if ok: + return {"kind": "step", "table": plan.anchor, "column": col.name} + return None + + +def _audit_source( + schema: Schema, plan: KindPlan, actor_values: Mapping[str, str] +) -> Optional[dict[str, Any]]: + """A table that gained, in every update demonstration, a row naming the + main record (a column holding its key) and the acting user (a column + holding the actor's id): the app's own audit trail of saves (EspoCRM's + ``action_history_record``, Dolibarr's agenda events).""" + + t = schema.tables[plan.anchor] + if len(t.primary_key) != 1 or not actor_values: + return None + actors = set(actor_values.values()) + found: Optional[dict[str, Any]] = None + for name in sorted(schema.tables): + table = schema.tables[name] + if name == plan.anchor or len(table.primary_key) != 1: + continue + pairs: Optional[set[tuple[str, str, str]]] = None + rows: list[dict[str, Optional[str]]] = [] + for d, a in zip(plan.demos, plan.anchors): + key = (a.after or {}).get(t.primary_key[0]) + here: set[tuple[str, str, str]] = set() + for ch in d.diff.of(name, "added"): + r = ch.after or {} + for lc, lv in r.items(): + if lv != key: + continue + for ac, av in r.items(): + if ac != lc and av in actors: + p = next(k for k, v in actor_values.items() if v == av) + here.add((lc, ac, p)) + rows.append(r) + pairs = here if pairs is None else pairs & here + if not pairs or len(pairs) != 1: + continue + lc, ac, param = next(iter(pairs)) + # Rows of the same type: their constant text and whole-number columns. + where = {} + for col in table.columns: + if col.name in (lc, ac) or col.name in table.primary_key: + continue + values = {r.get(col.name) for r in rows} + if len(values) == 1: + v = next(iter(values)) + if v is not None and (col.kind == "text" or _WHOLE_NUMBER.match(v)): + if "\\" not in v and "'" not in v and "%" not in v: + where[col.name] = v + found = { + "kind": "audit", + "table": name, + "record_column": lc, + "author_column": ac, + "actor_param": param, + "where": where, + } + break + return found + + +def _versions_frame( + b: "SqlBuilder", plan: KindPlan, settings: "Settings" +) -> Optional[dict[str, Any]]: + v = plan.versions + if v is None: + return None + pk = b.schema.tables[plan.anchor].primary_key[0] + if v["kind"] == "step": + sql = ( + f"SELECT {b.text('t', plan.anchor, pk)} AS {b.q('id')}, " + f"{b.text('t', plan.anchor, v['column'])} AS {b.q('ver')} " + f"FROM {b.tref(plan.anchor)} t" + ) + return { + "read": {"kind": "sql", **settings.read, "sql_query": sql}, + "record_field": "id", + "target_field": "id", + "version_field": "ver", + } + table = v["table"] + vpk = b.schema.tables[table].primary_key[0] + conds = " AND ".join( + f"{b.text('t', table, c)} = {b.literal(val)}" + for c, val in sorted(v["where"].items()) + ) + sql = ( + f"SELECT {b.text('t', table, v['record_column'])} AS {b.q('id')}, " + f"{b.text('t', table, vpk)} AS {b.q('ver')}, " + f"{b.text('t', table, v['author_column'])} AS {b.q('author')} " + f"FROM {b.tref(table)} t" + (f" WHERE {conds}" if conds else "") + ) + return { + "read": {"kind": "sql", **settings.read, "sql_query": sql}, + "record_field": "id", + "target_field": "id", + "version_field": "ver", + "author_field": "author", + "self": [{"param": v["actor_param"]}], + } def _mark_replaced( @@ -1476,6 +1615,12 @@ def build_contract( frame["target_fields"] = [] else: frame["target_fields"] = sorted(target_fields) + if plan.mode == "update": + # Another user's save to the target inside the window (a lost + # update) shows only in the target's versions. + versions = _versions_frame(b, plan, settings) + if versions is not None: + frame["versions"] = versions if schema.dialect == "mysql": allow: list[dict[str, Any]] = [] for t in sorted(logs): @@ -2418,6 +2563,7 @@ def propose( k: v.describe(p.anchor) for k, v in sorted(p.locations.items()) }, "placed_by_name": sorted(p.by_name), + "versions": p.versions, "placed_from_another_kind": sorted(p.shared), "demonstrations": len(p.demos), "children": {k: g.table for k, g in p.children.items()}, diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index 20475c3bf8..1d5b5aec92 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -337,6 +337,10 @@ def test_questions_and_contracts(generated: dict[str, Any]) -> None: # notes), and the due date that create_issue's demonstrations placed on # the same table: every non-empty subset gets a contract. assert kinds["update_issue"]["placed_from_another_kind"] == ["due_date"] + # The journal names the issue and the user who saved it: the update's + # contracts read it as the target's versions (a lost update shows there). + assert kinds["update_issue"]["versions"]["kind"] == "audit" + assert kinds["update_issue"]["versions"]["table"] == "journals" assert kinds["update_issue"]["emitted"] == 31, json.dumps( kinds["update_issue"], indent=1 ) From bc399cac994c2da873f2e952bd00624470d57f7d Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 17:44:14 -0400 Subject: [PATCH 69/94] Generator results so far; CAPABILITIES; review brief --- CAPABILITIES.md | 2 +- llms-full.txt | 1 + .../benchmark/generate_contracts/RESULTS.md | 29 +- .../results/espocrm/QUESTIONS.md | 16 + .../results/espocrm/answers.yaml | 2 + .../results/espocrm/manifest.yaml | 2457 +++++++++++++++++ .../results/espocrm/report.json | 170 ++ 7 files changed, 2663 insertions(+), 14 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/report.json diff --git a/CAPABILITIES.md b/CAPABILITIES.md index f9dea9d75e..04b88ebfba 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,7 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, and a recheck window the person confirms. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies: Dolibarr 3 questions; RESULTS_PLACEHOLDER) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/llms-full.txt b/llms-full.txt index 433239090f..d43a2cae5f 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -544,6 +544,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/packages/flow/benchmark/generate_contracts/RESULTS.md b/packages/flow/benchmark/generate_contracts/RESULTS.md index 233ce40111..4c0c0e5817 100644 --- a/packages/flow/benchmark/generate_contracts/RESULTS.md +++ b/packages/flow/benchmark/generate_contracts/RESULTS.md @@ -22,14 +22,14 @@ Late-write faults also ran the recheck the contract declares. | | Dolibarr | Redmine | EspoCRM | | --- | --- | --- | --- | | Database | MariaDB 11.4 | PostgreSQL | MariaDB | -| Questions for a person | **3** | **1** | **ESPO_Q** | +| Questions for a person | **3** | **1** | **2** | | First study, person decisions | 11 | 11 | 11 | -| Contracts written | 93 | 95 | ESPO_N | -| Correct writes confirmed (tasks and controls) | 40/43 | 35/38 | ESPO_C | -| In-scope planted faults stopped | **22/22** | **19/19** | **ESPO_I** | +| Contracts written | 93 | 95 | 235 | +| Correct writes confirmed (tasks and controls) | 40/43 | 35/38 | 35/40 | +| In-scope planted faults stopped | **22/22** | **19/19** | **21/21** | | First study, in-scope faults stopped | 21/21 | 26/26 | 16/19 | -| Out-of-scope late writes stopped | 2/2 (recheck) | 0/2 (no recheck on PostgreSQL) | ESPO_O | -| `propose` time | 1.5 min | 2.5 min | ESPO_T | +| Out-of-scope late writes stopped | 2/2 (recheck) | 0/2 (no recheck on PostgreSQL) | 1/2 (recheck) | +| `propose` time | 1.5 min | 2.5 min | 2 min | The first study measured false stops on agents' writes; here the correct writes are the pack's reference writes and the controls, so the two false @@ -45,7 +45,7 @@ stop rates are not the same measure. as a log, `updated_on`, `lock_version`, `user_preferences.others`, and the time entry's `tyear`, `tmonth`, `tweek`). No recheck question: PostgreSQL has no whole-database frame. -- **EspoCRM** (ESPO_QN) +- **EspoCRM** (2): bookkeeping (stream notes, `call_user` and email link rows per write; `action_history_record`, `auth_log_record` and others as logs; `stream_updated_at`, `modified_at`); the recheck window (300 s). Every answer was the proposal, except the one choice in Dolibarr's question 3. @@ -53,7 +53,7 @@ Every answer was the proposal, except the one choice in Dolibarr's question 3. - Dolibarr and EspoCRM: `GRANT TRIGGER ON .* TO ` (`--grant-trigger`): the whole-database frame must see triggers. -- ESPO_SETUP +- EspoCRM before the SQL guard fix (#1665): a view `espocrm_oa_read.oa_read_call` (`--create-views`), because the guard refused the table name `call`. After the rebase the guard reads `call` and no view is needed. ### False stops, by cause @@ -65,12 +65,15 @@ Every answer was the proposal, except the one choice in Dolibarr's question 3. | Redmine | T19 | Closing an issue sets `closed_on`; no demonstration closed one | | Redmine | F21 (control) | Another user's edit to an unrelated issue: not attributed | | Redmine | F24 (control) | Two saves add two journals | -ESPO_FS +| EspoCRM | T21 | The reference write removes the old email address row when replacing it; the demonstration didn't | +| EspoCRM | T22 | `field: phone` has no contract: no demonstration, and phones live in another table | +| EspoCRM | F22, F27 (controls) | Another user's unrelated edit or new lead: not attributed | +| EspoCRM | F24 (control) | Mixed-case email and padded name (emails now compare case-folded) | ### Misses Out of scope only: Redmine F19 and F20 (late writes; PostgreSQL has no -recheck). ESPO_MISS No in-scope planted fault was confirmed on any app. +recheck). EspoCRM F21 (a late edit to a look-alike lead: an update's main table is read by its own surface, which the recheck doesn't read). No in-scope planted fault was confirmed on any app. ## Reproduce @@ -90,6 +93,6 @@ python summarize.py dolibarr WORK full set is regenerated by the commands above). The fault scripts and the oracle stay in the study's private pack. -Code: Dolibarr and Redmine ran on this branch at `a6b5a3915` with #1661 (the -whole-database frame fix) merged in; EspoCRM ran after the rebase on `main` -with #1661, #1662, #1663, #1665, #1666, #1668 and #1669. +Code: these runs used this branch before the rebase onto `main`, with #1661 +(the whole-database frame fix) merged in, and before `frame.versions` was +emitted. Reruns on the rebased branch are in progress. diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md new file mode 100644 index 0000000000..b176960544 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md @@ -0,0 +1,16 @@ +# Questions for a person + +1. Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): call_user (1 per write), note (1 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change): action_history_record, auth_log_record, note, note_user Columns that change on their own and are not compared: account: stream_updated_at; contact: stream_updated_at; lead: modified_at, modified_by_id, stream_updated_at Columns of a new record that differ between demonstrations and are not checked: call: parent_type, uid; opportunity: last_stage, probability Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. + - Answer key: `bookkeeping`. Proposed: `accept`. + - Answered: `"accept"` + +2. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. + - Answer key: `recheck_after_s`. Proposed: `300`. + - Answered: `300` + +## Kinds + +- `create_contact` (create, main table `contact`): 64 contract(s). +- `create_opportunity` (create, main table `opportunity`): 64 contract(s). +- `update_lead` (update, main table `lead`): 43 contract(s). +- `log_call` (create, main table `call`): 64 contract(s). diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml new file mode 100644 index 0000000000..7efebeb6dc --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml @@ -0,0 +1,2 @@ +bookkeeping: accept +recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml new file mode 100644 index 0000000000..5af4af6bac --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml @@ -0,0 +1,2457 @@ +schema_version: openadapt.contract-manifest/v1 +dialect: mysql +database: espocrm +actor_params: +- bot_user_id +questions_open: [] +kinds: + create_contact: + mode: create + main_table: contact + parameters: + - account + - account_city + - first_name + - last_name + - email + - phone + - title + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - account + - account_city + - email + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml + - fields: + - account + - account_city + - email + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+last_name.cc617045.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+last_name.cc617045.found.yaml + - fields: + - account_city + - email + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+last_name+phone+title.be4b0e8f.create.yaml + found: contracts/create_contact/account_city+email+first_name+last_name+phone+title.be4b0e8f.found.yaml + - fields: + - account + - email + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name+last_name+phone+title.ce919501.create.yaml + found: contracts/create_contact/account+email+first_name+last_name+phone+title.ce919501.found.yaml + - fields: + - account + - account_city + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name+last_name+phone+title.4ff4a105.create.yaml + found: contracts/create_contact/account+account_city+first_name+last_name+phone+title.4ff4a105.found.yaml + - fields: + - account + - account_city + - email + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+last_name+phone+title.41e87667.create.yaml + found: contracts/create_contact/account+account_city+email+last_name+phone+title.41e87667.found.yaml + - fields: + - account + - account_city + - email + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+phone+title.308596e1.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+phone+title.308596e1.found.yaml + - fields: + - account + - account_city + - email + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+last_name+title.ef6b5a09.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+last_name+title.ef6b5a09.found.yaml + - fields: + - account + - account_city + - email + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+last_name+phone.66680a4a.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+last_name+phone.66680a4a.found.yaml + - fields: + - account_city + - email + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+last_name.e363a157.create.yaml + found: contracts/create_contact/account_city+email+first_name+last_name.e363a157.found.yaml + - fields: + - account + - email + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name+last_name.8a4c37a1.create.yaml + found: contracts/create_contact/account+email+first_name+last_name.8a4c37a1.found.yaml + - fields: + - account + - account_city + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name+last_name.0890bdcf.create.yaml + found: contracts/create_contact/account+account_city+first_name+last_name.0890bdcf.found.yaml + - fields: + - account + - account_city + - email + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+last_name.0663f8b5.create.yaml + found: contracts/create_contact/account+account_city+email+last_name.0663f8b5.found.yaml + - fields: + - account + - account_city + - email + - first_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name.73be94d9.create.yaml + found: contracts/create_contact/account+account_city+email+first_name.73be94d9.found.yaml + - fields: + - email + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/email+first_name+last_name+phone+title.b657aeb5.create.yaml + found: contracts/create_contact/email+first_name+last_name+phone+title.b657aeb5.found.yaml + - fields: + - account_city + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+first_name+last_name+phone+title.dc8ca344.create.yaml + found: contracts/create_contact/account_city+first_name+last_name+phone+title.dc8ca344.found.yaml + - fields: + - account_city + - email + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+last_name+phone+title.70958d35.create.yaml + found: contracts/create_contact/account_city+email+last_name+phone+title.70958d35.found.yaml + - fields: + - account_city + - email + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+phone+title.bcc47205.create.yaml + found: contracts/create_contact/account_city+email+first_name+phone+title.bcc47205.found.yaml + - fields: + - account_city + - email + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+last_name+title.4b29e4b5.create.yaml + found: contracts/create_contact/account_city+email+first_name+last_name+title.4b29e4b5.found.yaml + - fields: + - account_city + - email + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+last_name+phone.7fc1c0d6.create.yaml + found: contracts/create_contact/account_city+email+first_name+last_name+phone.7fc1c0d6.found.yaml + - fields: + - account + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+first_name+last_name+phone+title.355dafc1.create.yaml + found: contracts/create_contact/account+first_name+last_name+phone+title.355dafc1.found.yaml + - fields: + - account + - email + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+last_name+phone+title.a4c2b999.create.yaml + found: contracts/create_contact/account+email+last_name+phone+title.a4c2b999.found.yaml + - fields: + - account + - email + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name+phone+title.8cf06d5a.create.yaml + found: contracts/create_contact/account+email+first_name+phone+title.8cf06d5a.found.yaml + - fields: + - account + - email + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name+last_name+title.e9c71bcd.create.yaml + found: contracts/create_contact/account+email+first_name+last_name+title.e9c71bcd.found.yaml + - fields: + - account + - email + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name+last_name+phone.77aad286.create.yaml + found: contracts/create_contact/account+email+first_name+last_name+phone.77aad286.found.yaml + - fields: + - account + - account_city + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+last_name+phone+title.0f22d92a.create.yaml + found: contracts/create_contact/account+account_city+last_name+phone+title.0f22d92a.found.yaml + - fields: + - account + - account_city + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name+phone+title.9c11906f.create.yaml + found: contracts/create_contact/account+account_city+first_name+phone+title.9c11906f.found.yaml + - fields: + - account + - account_city + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name+last_name+title.e278e40a.create.yaml + found: contracts/create_contact/account+account_city+first_name+last_name+title.e278e40a.found.yaml + - fields: + - account + - account_city + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name+last_name+phone.29f540dc.create.yaml + found: contracts/create_contact/account+account_city+first_name+last_name+phone.29f540dc.found.yaml + - fields: + - account + - account_city + - email + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+phone+title.96539c9c.create.yaml + found: contracts/create_contact/account+account_city+email+phone+title.96539c9c.found.yaml + - fields: + - account + - account_city + - email + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+last_name+title.58914f1d.create.yaml + found: contracts/create_contact/account+account_city+email+last_name+title.58914f1d.found.yaml + - fields: + - account + - account_city + - email + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+last_name+phone.dc15be7c.create.yaml + found: contracts/create_contact/account+account_city+email+last_name+phone.dc15be7c.found.yaml + - fields: + - account + - account_city + - email + - first_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+title.bf9dcd88.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+title.bf9dcd88.found.yaml + - fields: + - account + - account_city + - email + - first_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email+first_name+phone.01d2568c.create.yaml + found: contracts/create_contact/account+account_city+email+first_name+phone.01d2568c.found.yaml + - fields: + - email + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/email+first_name+last_name.c3fc7806.create.yaml + found: contracts/create_contact/email+first_name+last_name.c3fc7806.found.yaml + - fields: + - account_city + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+first_name+last_name.b6730c1e.create.yaml + found: contracts/create_contact/account_city+first_name+last_name.b6730c1e.found.yaml + - fields: + - account_city + - email + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+last_name.116193ea.create.yaml + found: contracts/create_contact/account_city+email+last_name.116193ea.found.yaml + - fields: + - account_city + - email + - first_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name.0382cd38.create.yaml + found: contracts/create_contact/account_city+email+first_name.0382cd38.found.yaml + - fields: + - account + - first_name + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+first_name+last_name.646d1dd3.create.yaml + found: contracts/create_contact/account+first_name+last_name.646d1dd3.found.yaml + - fields: + - account + - email + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+last_name.cddfc397.create.yaml + found: contracts/create_contact/account+email+last_name.cddfc397.found.yaml + - fields: + - account + - email + - first_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+first_name.89ddee04.create.yaml + found: contracts/create_contact/account+email+first_name.89ddee04.found.yaml + - fields: + - account + - account_city + - last_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+last_name.31fe521c.create.yaml + found: contracts/create_contact/account+account_city+last_name.31fe521c.found.yaml + - fields: + - account + - account_city + - first_name + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+first_name.94c8b13b.create.yaml + found: contracts/create_contact/account+account_city+first_name.94c8b13b.found.yaml + - fields: + - account + - account_city + - email + lists: {} + dispatch_value: null + create: contracts/create_contact/account+account_city+email.a039b55f.create.yaml + found: contracts/create_contact/account+account_city+email.a039b55f.found.yaml + - fields: + - first_name + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/first_name+last_name+phone+title.759e589f.create.yaml + found: contracts/create_contact/first_name+last_name+phone+title.759e589f.found.yaml + - fields: + - email + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/email+last_name+phone+title.df1cb621.create.yaml + found: contracts/create_contact/email+last_name+phone+title.df1cb621.found.yaml + - fields: + - email + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/email+first_name+phone+title.4eb30bed.create.yaml + found: contracts/create_contact/email+first_name+phone+title.4eb30bed.found.yaml + - fields: + - email + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/email+first_name+last_name+title.44a56b67.create.yaml + found: contracts/create_contact/email+first_name+last_name+title.44a56b67.found.yaml + - fields: + - email + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/email+first_name+last_name+phone.46fdda46.create.yaml + found: contracts/create_contact/email+first_name+last_name+phone.46fdda46.found.yaml + - fields: + - account_city + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+last_name+phone+title.88c48587.create.yaml + found: contracts/create_contact/account_city+last_name+phone+title.88c48587.found.yaml + - fields: + - account_city + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+first_name+phone+title.79e3e404.create.yaml + found: contracts/create_contact/account_city+first_name+phone+title.79e3e404.found.yaml + - fields: + - account_city + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+first_name+last_name+title.869091a0.create.yaml + found: contracts/create_contact/account_city+first_name+last_name+title.869091a0.found.yaml + - fields: + - account_city + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+first_name+last_name+phone.e7cd2a47.create.yaml + found: contracts/create_contact/account_city+first_name+last_name+phone.e7cd2a47.found.yaml + - fields: + - account_city + - email + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+phone+title.667b64eb.create.yaml + found: contracts/create_contact/account_city+email+phone+title.667b64eb.found.yaml + - fields: + - account_city + - email + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+last_name+title.72180e70.create.yaml + found: contracts/create_contact/account_city+email+last_name+title.72180e70.found.yaml + - fields: + - account_city + - email + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+last_name+phone.7cf3ab30.create.yaml + found: contracts/create_contact/account_city+email+last_name+phone.7cf3ab30.found.yaml + - fields: + - account_city + - email + - first_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+title.c501b9af.create.yaml + found: contracts/create_contact/account_city+email+first_name+title.c501b9af.found.yaml + - fields: + - account_city + - email + - first_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account_city+email+first_name+phone.9350202d.create.yaml + found: contracts/create_contact/account_city+email+first_name+phone.9350202d.found.yaml + - fields: + - account + - last_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+last_name+phone+title.6c3a70e7.create.yaml + found: contracts/create_contact/account+last_name+phone+title.6c3a70e7.found.yaml + - fields: + - account + - first_name + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+first_name+phone+title.441099c8.create.yaml + found: contracts/create_contact/account+first_name+phone+title.441099c8.found.yaml + - fields: + - account + - first_name + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+first_name+last_name+title.d24613f1.create.yaml + found: contracts/create_contact/account+first_name+last_name+title.d24613f1.found.yaml + - fields: + - account + - first_name + - last_name + - phone + lists: {} + dispatch_value: null + create: contracts/create_contact/account+first_name+last_name+phone.83f6dc7e.create.yaml + found: contracts/create_contact/account+first_name+last_name+phone.83f6dc7e.found.yaml + - fields: + - account + - email + - phone + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+phone+title.a4ceda1e.create.yaml + found: contracts/create_contact/account+email+phone+title.a4ceda1e.found.yaml + - fields: + - account + - email + - last_name + - title + lists: {} + dispatch_value: null + create: contracts/create_contact/account+email+last_name+title.ac86c876.create.yaml + found: contracts/create_contact/account+email+last_name+title.ac86c876.found.yaml + create_opportunity: + mode: create + main_table: opportunity + parameters: + - account + - account_city + - name + - amount + - currency + - stage + - close_date + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - account + - account_city + - amount + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml + - fields: + - account_city + - amount + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+currency+name+stage.3e8d919d.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+currency+name+stage.3e8d919d.found.yaml + - fields: + - account + - amount + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+currency+name+stage.ee61cdb0.create.yaml + found: contracts/create_opportunity/account+amount+close_date+currency+name+stage.ee61cdb0.found.yaml + - fields: + - account + - account_city + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+currency+name+stage.e6646c16.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+currency+name+stage.e6646c16.found.yaml + - fields: + - account + - account_city + - amount + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+currency+name+stage.1c3e81bb.create.yaml + found: contracts/create_opportunity/account+account_city+amount+currency+name+stage.1c3e81bb.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+name+stage.840c2e09.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+name+stage.840c2e09.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+currency+stage.2110b795.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+currency+stage.2110b795.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+currency+name.3d88d046.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+currency+name.3d88d046.found.yaml + - fields: + - amount + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/amount+close_date+currency+name+stage.636367d7.create.yaml + found: contracts/create_opportunity/amount+close_date+currency+name+stage.636367d7.found.yaml + - fields: + - account_city + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+close_date+currency+name+stage.18017a0a.create.yaml + found: contracts/create_opportunity/account_city+close_date+currency+name+stage.18017a0a.found.yaml + - fields: + - account_city + - amount + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+currency+name+stage.49fd1ee5.create.yaml + found: contracts/create_opportunity/account_city+amount+currency+name+stage.49fd1ee5.found.yaml + - fields: + - account_city + - amount + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+name+stage.74c4d8cd.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+name+stage.74c4d8cd.found.yaml + - fields: + - account_city + - amount + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+currency+stage.157c0c72.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+currency+stage.157c0c72.found.yaml + - fields: + - account_city + - amount + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+currency+name.6310e515.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+currency+name.6310e515.found.yaml + - fields: + - account + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+close_date+currency+name+stage.b4e0a253.create.yaml + found: contracts/create_opportunity/account+close_date+currency+name+stage.b4e0a253.found.yaml + - fields: + - account + - amount + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+currency+name+stage.b105c423.create.yaml + found: contracts/create_opportunity/account+amount+currency+name+stage.b105c423.found.yaml + - fields: + - account + - amount + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+name+stage.a21562d9.create.yaml + found: contracts/create_opportunity/account+amount+close_date+name+stage.a21562d9.found.yaml + - fields: + - account + - amount + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+currency+stage.c8c627a9.create.yaml + found: contracts/create_opportunity/account+amount+close_date+currency+stage.c8c627a9.found.yaml + - fields: + - account + - amount + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+currency+name.a75ae1d8.create.yaml + found: contracts/create_opportunity/account+amount+close_date+currency+name.a75ae1d8.found.yaml + - fields: + - account + - account_city + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+currency+name+stage.221bd3cc.create.yaml + found: contracts/create_opportunity/account+account_city+currency+name+stage.221bd3cc.found.yaml + - fields: + - account + - account_city + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+name+stage.bce513f8.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+name+stage.bce513f8.found.yaml + - fields: + - account + - account_city + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+currency+stage.9f763456.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+currency+stage.9f763456.found.yaml + - fields: + - account + - account_city + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+currency+name.53170b23.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+currency+name.53170b23.found.yaml + - fields: + - account + - account_city + - amount + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+name+stage.940c25af.create.yaml + found: contracts/create_opportunity/account+account_city+amount+name+stage.940c25af.found.yaml + - fields: + - account + - account_city + - amount + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+currency+stage.f84f8946.create.yaml + found: contracts/create_opportunity/account+account_city+amount+currency+stage.f84f8946.found.yaml + - fields: + - account + - account_city + - amount + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+currency+name.9d5c7892.create.yaml + found: contracts/create_opportunity/account+account_city+amount+currency+name.9d5c7892.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+stage.4fdcd48d.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+stage.4fdcd48d.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+name.c265ff6a.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+name.c265ff6a.found.yaml + - fields: + - account + - account_city + - amount + - close_date + - currency + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date+currency.19ea66ae.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date+currency.19ea66ae.found.yaml + - fields: + - close_date + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/close_date+currency+name+stage.6196f741.create.yaml + found: contracts/create_opportunity/close_date+currency+name+stage.6196f741.found.yaml + - fields: + - amount + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/amount+currency+name+stage.bd43a82e.create.yaml + found: contracts/create_opportunity/amount+currency+name+stage.bd43a82e.found.yaml + - fields: + - amount + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/amount+close_date+name+stage.2a87af45.create.yaml + found: contracts/create_opportunity/amount+close_date+name+stage.2a87af45.found.yaml + - fields: + - amount + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/amount+close_date+currency+stage.d62f80cd.create.yaml + found: contracts/create_opportunity/amount+close_date+currency+stage.d62f80cd.found.yaml + - fields: + - amount + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/amount+close_date+currency+name.6c521040.create.yaml + found: contracts/create_opportunity/amount+close_date+currency+name.6c521040.found.yaml + - fields: + - account_city + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+currency+name+stage.b7663184.create.yaml + found: contracts/create_opportunity/account_city+currency+name+stage.b7663184.found.yaml + - fields: + - account_city + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+close_date+name+stage.d3bed3d5.create.yaml + found: contracts/create_opportunity/account_city+close_date+name+stage.d3bed3d5.found.yaml + - fields: + - account_city + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+close_date+currency+stage.cc489929.create.yaml + found: contracts/create_opportunity/account_city+close_date+currency+stage.cc489929.found.yaml + - fields: + - account_city + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+close_date+currency+name.0ec4ae28.create.yaml + found: contracts/create_opportunity/account_city+close_date+currency+name.0ec4ae28.found.yaml + - fields: + - account_city + - amount + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+name+stage.44542cc2.create.yaml + found: contracts/create_opportunity/account_city+amount+name+stage.44542cc2.found.yaml + - fields: + - account_city + - amount + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+currency+stage.01773cbe.create.yaml + found: contracts/create_opportunity/account_city+amount+currency+stage.01773cbe.found.yaml + - fields: + - account_city + - amount + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+currency+name.964d6501.create.yaml + found: contracts/create_opportunity/account_city+amount+currency+name.964d6501.found.yaml + - fields: + - account_city + - amount + - close_date + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+stage.05eb4131.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+stage.05eb4131.found.yaml + - fields: + - account_city + - amount + - close_date + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+name.691edf71.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+name.691edf71.found.yaml + - fields: + - account_city + - amount + - close_date + - currency + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account_city+amount+close_date+currency.5f3eb90a.create.yaml + found: contracts/create_opportunity/account_city+amount+close_date+currency.5f3eb90a.found.yaml + - fields: + - account + - currency + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+currency+name+stage.ef841eea.create.yaml + found: contracts/create_opportunity/account+currency+name+stage.ef841eea.found.yaml + - fields: + - account + - close_date + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+close_date+name+stage.9dec0d83.create.yaml + found: contracts/create_opportunity/account+close_date+name+stage.9dec0d83.found.yaml + - fields: + - account + - close_date + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+close_date+currency+stage.67034ba2.create.yaml + found: contracts/create_opportunity/account+close_date+currency+stage.67034ba2.found.yaml + - fields: + - account + - close_date + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+close_date+currency+name.430d7e5a.create.yaml + found: contracts/create_opportunity/account+close_date+currency+name.430d7e5a.found.yaml + - fields: + - account + - amount + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+name+stage.cc3f78e3.create.yaml + found: contracts/create_opportunity/account+amount+name+stage.cc3f78e3.found.yaml + - fields: + - account + - amount + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+currency+stage.d361f29b.create.yaml + found: contracts/create_opportunity/account+amount+currency+stage.d361f29b.found.yaml + - fields: + - account + - amount + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+currency+name.9346646b.create.yaml + found: contracts/create_opportunity/account+amount+currency+name.9346646b.found.yaml + - fields: + - account + - amount + - close_date + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+stage.b840114e.create.yaml + found: contracts/create_opportunity/account+amount+close_date+stage.b840114e.found.yaml + - fields: + - account + - amount + - close_date + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+name.510970cc.create.yaml + found: contracts/create_opportunity/account+amount+close_date+name.510970cc.found.yaml + - fields: + - account + - amount + - close_date + - currency + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+amount+close_date+currency.add6b978.create.yaml + found: contracts/create_opportunity/account+amount+close_date+currency.add6b978.found.yaml + - fields: + - account + - account_city + - name + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+name+stage.79f9642c.create.yaml + found: contracts/create_opportunity/account+account_city+name+stage.79f9642c.found.yaml + - fields: + - account + - account_city + - currency + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+currency+stage.ab0ce4e4.create.yaml + found: contracts/create_opportunity/account+account_city+currency+stage.ab0ce4e4.found.yaml + - fields: + - account + - account_city + - currency + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+currency+name.37d28d7b.create.yaml + found: contracts/create_opportunity/account+account_city+currency+name.37d28d7b.found.yaml + - fields: + - account + - account_city + - close_date + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+stage.e2243a69.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+stage.e2243a69.found.yaml + - fields: + - account + - account_city + - close_date + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+name.7eba75e7.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+name.7eba75e7.found.yaml + - fields: + - account + - account_city + - close_date + - currency + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+close_date+currency.f4da1bc2.create.yaml + found: contracts/create_opportunity/account+account_city+close_date+currency.f4da1bc2.found.yaml + - fields: + - account + - account_city + - amount + - stage + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+stage.ef3af8a2.create.yaml + found: contracts/create_opportunity/account+account_city+amount+stage.ef3af8a2.found.yaml + - fields: + - account + - account_city + - amount + - name + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+name.898ff6d9.create.yaml + found: contracts/create_opportunity/account+account_city+amount+name.898ff6d9.found.yaml + - fields: + - account + - account_city + - amount + - currency + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+currency.54e9d8de.create.yaml + found: contracts/create_opportunity/account+account_city+amount+currency.54e9d8de.found.yaml + - fields: + - account + - account_city + - amount + - close_date + lists: {} + dispatch_value: null + create: contracts/create_opportunity/account+account_city+amount+close_date.08f5c040.create.yaml + found: contracts/create_opportunity/account+account_city+amount+close_date.08f5c040.found.yaml + update_lead: + mode: update + main_table: lead + parameters: + - first_name + - last_name + - company + - field + - value + identity: + - company + - first_name + - last_name + lists: [] + dispatch: field + variants: + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: accountName + update: contracts/update_lead/accountName+company+field+first_name+last_name+value.f0188cc9.update.yaml + found: contracts/update_lead/accountName+company+field+first_name+last_name+value.f0188cc9.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: account_name + update: contracts/update_lead/account_name+company+field+first_name+last_name+value.112a6b3c.update.yaml + found: contracts/update_lead/account_name+company+field+first_name+last_name+value.112a6b3c.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: addressCity + update: contracts/update_lead/addressCity+company+field+first_name+last_name+value.2e349c1f.update.yaml + found: contracts/update_lead/addressCity+company+field+first_name+last_name+value.2e349c1f.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: addressCountry + update: contracts/update_lead/addressCountry+company+field+first_name+last_name+value.cf5a4b98.update.yaml + found: contracts/update_lead/addressCountry+company+field+first_name+last_name+value.cf5a4b98.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: addressPostalCode + update: contracts/update_lead/addressPostalCode+company+field+first_name+last_name+value.1b559d01.update.yaml + found: contracts/update_lead/addressPostalCode+company+field+first_name+last_name+value.1b559d01.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: addressState + update: contracts/update_lead/addressState+company+field+first_name+last_name+value.d8fcca02.update.yaml + found: contracts/update_lead/addressState+company+field+first_name+last_name+value.d8fcca02.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: addressStreet + update: contracts/update_lead/addressStreet+company+field+first_name+last_name+value.ccdba0fe.update.yaml + found: contracts/update_lead/addressStreet+company+field+first_name+last_name+value.ccdba0fe.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: address_city + update: contracts/update_lead/address_city+company+field+first_name+last_name+value.39516a5c.update.yaml + found: contracts/update_lead/address_city+company+field+first_name+last_name+value.39516a5c.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: address_country + update: contracts/update_lead/address_country+company+field+first_name+last_name+value.26370712.update.yaml + found: contracts/update_lead/address_country+company+field+first_name+last_name+value.26370712.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: address_postal_code + update: contracts/update_lead/address_postal_code+company+field+first_name+last_name+value.b15ffc5b.update.yaml + found: contracts/update_lead/address_postal_code+company+field+first_name+last_name+value.b15ffc5b.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: address_state + update: contracts/update_lead/address_state+company+field+first_name+last_name+value.787f1586.update.yaml + found: contracts/update_lead/address_state+company+field+first_name+last_name+value.787f1586.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: address_street + update: contracts/update_lead/address_street+company+field+first_name+last_name+value.abad9297.update.yaml + found: contracts/update_lead/address_street+company+field+first_name+last_name+value.abad9297.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: assignedUserId + update: contracts/update_lead/assignedUserId+company+field+first_name+last_name+value.d92ef246.update.yaml + found: contracts/update_lead/assignedUserId+company+field+first_name+last_name+value.d92ef246.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: assigned_user_id + update: contracts/update_lead/assigned_user_id+company+field+first_name+last_name+value.e65072f2.update.yaml + found: contracts/update_lead/assigned_user_id+company+field+first_name+last_name+value.e65072f2.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: campaignId + update: contracts/update_lead/campaignId+company+field+first_name+last_name+value.5d7c3f99.update.yaml + found: contracts/update_lead/campaignId+company+field+first_name+last_name+value.5d7c3f99.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: campaign_id + update: contracts/update_lead/campaign_id+company+field+first_name+last_name+value.2ba0bedb.update.yaml + found: contracts/update_lead/campaign_id+company+field+first_name+last_name+value.2ba0bedb.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: createdAccountId + update: contracts/update_lead/createdAccountId+company+field+first_name+last_name+value.1e11a836.update.yaml + found: contracts/update_lead/createdAccountId+company+field+first_name+last_name+value.1e11a836.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: createdById + update: contracts/update_lead/createdById+company+field+first_name+last_name+value.ce7b1b24.update.yaml + found: contracts/update_lead/createdById+company+field+first_name+last_name+value.ce7b1b24.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: createdContactId + update: contracts/update_lead/createdContactId+company+field+first_name+last_name+value.5e8f2aec.update.yaml + found: contracts/update_lead/createdContactId+company+field+first_name+last_name+value.5e8f2aec.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: createdOpportunityId + update: contracts/update_lead/createdOpportunityId+company+field+first_name+last_name+value.57ce21be.update.yaml + found: contracts/update_lead/createdOpportunityId+company+field+first_name+last_name+value.57ce21be.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: created_account_id + update: contracts/update_lead/created_account_id+company+field+first_name+last_name+value.61ccbd38.update.yaml + found: contracts/update_lead/created_account_id+company+field+first_name+last_name+value.61ccbd38.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: created_by_id + update: contracts/update_lead/created_by_id+company+field+first_name+last_name+value.afd73f3f.update.yaml + found: contracts/update_lead/created_by_id+company+field+first_name+last_name+value.afd73f3f.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: created_contact_id + update: contracts/update_lead/created_contact_id+company+field+first_name+last_name+value.46f293a5.update.yaml + found: contracts/update_lead/created_contact_id+company+field+first_name+last_name+value.46f293a5.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: created_opportunity_id + update: contracts/update_lead/created_opportunity_id+company+field+first_name+last_name+value.45f20089.update.yaml + found: contracts/update_lead/created_opportunity_id+company+field+first_name+last_name+value.45f20089.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: description + update: contracts/update_lead/description+company+field+first_name+last_name+value.f7f378eb.update.yaml + found: contracts/update_lead/description+company+field+first_name+last_name+value.f7f378eb.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: email + update: contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml + found: contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: firstName + update: contracts/update_lead/firstName+company+field+first_name+last_name+value.df020818.update.yaml + found: contracts/update_lead/firstName+company+field+first_name+last_name+value.df020818.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: first_name + update: contracts/update_lead/first_name+company+field+first_name+last_name+value.fe20eac3.update.yaml + found: contracts/update_lead/first_name+company+field+first_name+last_name+value.fe20eac3.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: industry + update: contracts/update_lead/industry+company+field+first_name+last_name+value.681c4e87.update.yaml + found: contracts/update_lead/industry+company+field+first_name+last_name+value.681c4e87.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: lastName + update: contracts/update_lead/lastName+company+field+first_name+last_name+value.20ff2853.update.yaml + found: contracts/update_lead/lastName+company+field+first_name+last_name+value.20ff2853.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: last_name + update: contracts/update_lead/last_name+company+field+first_name+last_name+value.aa922c91.update.yaml + found: contracts/update_lead/last_name+company+field+first_name+last_name+value.aa922c91.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: middleName + update: contracts/update_lead/middleName+company+field+first_name+last_name+value.e12f42a6.update.yaml + found: contracts/update_lead/middleName+company+field+first_name+last_name+value.e12f42a6.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: middle_name + update: contracts/update_lead/middle_name+company+field+first_name+last_name+value.95efddfe.update.yaml + found: contracts/update_lead/middle_name+company+field+first_name+last_name+value.95efddfe.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: modifiedById + update: contracts/update_lead/modifiedById+company+field+first_name+last_name+value.1cbe39b1.update.yaml + found: contracts/update_lead/modifiedById+company+field+first_name+last_name+value.1cbe39b1.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: modified_by_id + update: contracts/update_lead/modified_by_id+company+field+first_name+last_name+value.f3a3d0bb.update.yaml + found: contracts/update_lead/modified_by_id+company+field+first_name+last_name+value.f3a3d0bb.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: opportunityAmountCurrency + update: contracts/update_lead/opportunityAmountCurrency+company+field+first_name+last_name+value.fe84c11f.update.yaml + found: contracts/update_lead/opportunityAmountCurrency+company+field+first_name+last_name+value.fe84c11f.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: opportunity_amount_currency + update: contracts/update_lead/opportunity_amount_currency+company+field+first_name+last_name+value.b0a33e68.update.yaml + found: contracts/update_lead/opportunity_amount_currency+company+field+first_name+last_name+value.b0a33e68.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: salutationName + update: contracts/update_lead/salutationName+company+field+first_name+last_name+value.f6ad2278.update.yaml + found: contracts/update_lead/salutationName+company+field+first_name+last_name+value.f6ad2278.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: salutation_name + update: contracts/update_lead/salutation_name+company+field+first_name+last_name+value.f8404b60.update.yaml + found: contracts/update_lead/salutation_name+company+field+first_name+last_name+value.f8404b60.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: source + update: contracts/update_lead/source+company+field+first_name+last_name+value.e5ca3051.update.yaml + found: contracts/update_lead/source+company+field+first_name+last_name+value.e5ca3051.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: status + update: contracts/update_lead/status+company+field+first_name+last_name+value.d77469a2.update.yaml + found: contracts/update_lead/status+company+field+first_name+last_name+value.d77469a2.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: title + update: contracts/update_lead/title+company+field+first_name+last_name+value.39d15090.update.yaml + found: contracts/update_lead/title+company+field+first_name+last_name+value.39d15090.found.yaml + - fields: + - company + - field + - first_name + - last_name + - value + lists: {} + dispatch_value: website + update: contracts/update_lead/website+company+field+first_name+last_name+value.fdf2495b.update.yaml + found: contracts/update_lead/website+company+field+first_name+last_name+value.fdf2495b.found.yaml + log_call: + mode: create + main_table: call + parameters: + - subject + - status + - direction + - date_start + - duration_minutes + - account + - account_city + - contact + - lead + - lead_company + - description + identity: [] + lists: [] + dispatch: null + variants: + - fields: + - account + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status+subject.1c40818d.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status+subject.1c40818d.found.yaml + - fields: + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status+su.a1e6dea7.create.yaml + found: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status+su.a1e6dea7.found.yaml + - fields: + - account + - contact + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status+subject.ac259c2d.create.yaml + found: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status+subject.ac259c2d.found.yaml + - fields: + - account + - account_city + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status+su.40bb5ef5.create.yaml + found: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status+su.40bb5ef5.found.yaml + - fields: + - account + - account_city + - contact + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status+subje.13cace1e.create.yaml + found: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status+subje.13cace1e.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status+subjec.6fa3c615.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status+subjec.6fa3c615.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status+subj.375289e9.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status+subj.375289e9.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.f9fb0e0d.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.f9fb0e0d.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.fa682ac0.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.fa682ac0.found.yaml + - fields: + - direction + - duration_minutes + - lead + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/direction+duration_minutes+lead+lead_company+status+subject.582a1975.create.yaml + found: contracts/log_call/direction+duration_minutes+lead+lead_company+status+subject.582a1975.found.yaml + - fields: + - date_start + - duration_minutes + - lead + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+duration_minutes+lead+lead_company+status+subject.cd43c2c6.create.yaml + found: contracts/log_call/date_start+duration_minutes+lead+lead_company+status+subject.cd43c2c6.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead_company+status+subject.d1a9d93e.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead_company+status+subject.d1a9d93e.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+status+subject.e3f86e48.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+status+subject.e3f86e48.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - lead_company + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+subject.7cd2c97e.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+subject.7cd2c97e.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - lead_company + - status + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status.fe5bab1b.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status.fe5bab1b.found.yaml + - fields: + - contact + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/contact+date_start+description+direction+duration_minutes+status+subject.ecb41244.create.yaml + found: contracts/log_call/contact+date_start+description+direction+duration_minutes+status+subject.ecb41244.found.yaml + - fields: + - account_city + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+date_start+description+direction+duration_minutes+status+subject.72b09e7c.create.yaml + found: contracts/log_call/account_city+date_start+description+direction+duration_minutes+status+subject.72b09e7c.found.yaml + - fields: + - account_city + - contact + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+description+direction+duration_minutes+status+subject.6027050a.create.yaml + found: contracts/log_call/account_city+contact+description+direction+duration_minutes+status+subject.6027050a.found.yaml + - fields: + - account_city + - contact + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+date_start+direction+duration_minutes+status+subject.fe2cf091.create.yaml + found: contracts/log_call/account_city+contact+date_start+direction+duration_minutes+status+subject.fe2cf091.found.yaml + - fields: + - account_city + - contact + - date_start + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+date_start+description+duration_minutes+status+subject.933068cc.create.yaml + found: contracts/log_call/account_city+contact+date_start+description+duration_minutes+status+subject.933068cc.found.yaml + - fields: + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+subject.53ca7468.create.yaml + found: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+subject.53ca7468.found.yaml + - fields: + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status.502d3047.create.yaml + found: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status.502d3047.found.yaml + - fields: + - account + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+date_start+description+direction+duration_minutes+status+subject.461a3bbb.create.yaml + found: contracts/log_call/account+date_start+description+direction+duration_minutes+status+subject.461a3bbb.found.yaml + - fields: + - account + - contact + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+description+direction+duration_minutes+status+subject.4eeb71c8.create.yaml + found: contracts/log_call/account+contact+description+direction+duration_minutes+status+subject.4eeb71c8.found.yaml + - fields: + - account + - contact + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+date_start+direction+duration_minutes+status+subject.88c05db0.create.yaml + found: contracts/log_call/account+contact+date_start+direction+duration_minutes+status+subject.88c05db0.found.yaml + - fields: + - account + - contact + - date_start + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+date_start+description+duration_minutes+status+subject.92c40c7a.create.yaml + found: contracts/log_call/account+contact+date_start+description+duration_minutes+status+subject.92c40c7a.found.yaml + - fields: + - account + - contact + - date_start + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+subject.36ca60f2.create.yaml + found: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+subject.36ca60f2.found.yaml + - fields: + - account + - contact + - date_start + - description + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status.f18701ec.create.yaml + found: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status.f18701ec.found.yaml + - fields: + - account + - account_city + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+description+direction+duration_minutes+status+subject.2e513d2e.create.yaml + found: contracts/log_call/account+account_city+description+direction+duration_minutes+status+subject.2e513d2e.found.yaml + - fields: + - account + - account_city + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+date_start+direction+duration_minutes+status+subject.5dd1577d.create.yaml + found: contracts/log_call/account+account_city+date_start+direction+duration_minutes+status+subject.5dd1577d.found.yaml + - fields: + - account + - account_city + - date_start + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+date_start+description+duration_minutes+status+subject.c2f5c493.create.yaml + found: contracts/log_call/account+account_city+date_start+description+duration_minutes+status+subject.c2f5c493.found.yaml + - fields: + - account + - account_city + - date_start + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+subject.1444168f.create.yaml + found: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+subject.1444168f.found.yaml + - fields: + - account + - account_city + - date_start + - description + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status.6b8a69ef.create.yaml + found: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status.6b8a69ef.found.yaml + - fields: + - account + - account_city + - contact + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+direction+duration_minutes+status+subject.25d5283f.create.yaml + found: contracts/log_call/account+account_city+contact+direction+duration_minutes+status+subject.25d5283f.found.yaml + - fields: + - account + - account_city + - contact + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+description+duration_minutes+status+subject.994dc4c5.create.yaml + found: contracts/log_call/account+account_city+contact+description+duration_minutes+status+subject.994dc4c5.found.yaml + - fields: + - account + - account_city + - contact + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+subject.5213a5ad.create.yaml + found: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+subject.5213a5ad.found.yaml + - fields: + - account + - account_city + - contact + - description + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status.a5f10536.create.yaml + found: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status.a5f10536.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+duration_minutes+status+subject.a72c4dca.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+duration_minutes+status+subject.a72c4dca.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+subject.a261cf3e.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+subject.a261cf3e.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - direction + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status.97f875f0.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status.97f875f0.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+subject.2740ed8a.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+subject.2740ed8a.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - duration_minutes + - status + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status.eec71d6e.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status.eec71d6e.found.yaml + - fields: + - account + - account_city + - contact + - date_start + - description + - direction + - duration_minutes + lists: {} + dispatch_value: null + create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes.d2b14413.create.yaml + found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes.d2b14413.found.yaml + - fields: + - duration_minutes + - lead + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/duration_minutes+lead+lead_company+status+subject.248320eb.create.yaml + found: contracts/log_call/duration_minutes+lead+lead_company+status+subject.248320eb.found.yaml + - fields: + - direction + - duration_minutes + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/direction+duration_minutes+lead_company+status+subject.31e67761.create.yaml + found: contracts/log_call/direction+duration_minutes+lead_company+status+subject.31e67761.found.yaml + - fields: + - direction + - duration_minutes + - lead + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/direction+duration_minutes+lead+status+subject.80c440d6.create.yaml + found: contracts/log_call/direction+duration_minutes+lead+status+subject.80c440d6.found.yaml + - fields: + - direction + - duration_minutes + - lead + - lead_company + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/direction+duration_minutes+lead+lead_company+subject.de5e153e.create.yaml + found: contracts/log_call/direction+duration_minutes+lead+lead_company+subject.de5e153e.found.yaml + - fields: + - direction + - duration_minutes + - lead + - lead_company + - status + lists: {} + dispatch_value: null + create: contracts/log_call/direction+duration_minutes+lead+lead_company+status.0ab0a0ff.create.yaml + found: contracts/log_call/direction+duration_minutes+lead+lead_company+status.0ab0a0ff.found.yaml + - fields: + - date_start + - duration_minutes + - lead_company + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+duration_minutes+lead_company+status+subject.88eff1f9.create.yaml + found: contracts/log_call/date_start+duration_minutes+lead_company+status+subject.88eff1f9.found.yaml + - fields: + - date_start + - duration_minutes + - lead + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+duration_minutes+lead+status+subject.1bf286d5.create.yaml + found: contracts/log_call/date_start+duration_minutes+lead+status+subject.1bf286d5.found.yaml + - fields: + - date_start + - duration_minutes + - lead + - lead_company + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+duration_minutes+lead+lead_company+subject.f23cce08.create.yaml + found: contracts/log_call/date_start+duration_minutes+lead+lead_company+subject.f23cce08.found.yaml + - fields: + - date_start + - duration_minutes + - lead + - lead_company + - status + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+duration_minutes+lead+lead_company+status.5a81f788.create.yaml + found: contracts/log_call/date_start+duration_minutes+lead+lead_company+status.5a81f788.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+status+subject.023b89f7.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+status+subject.023b89f7.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead_company + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead_company+subject.8cb30c88.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead_company+subject.8cb30c88.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead_company + - status + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead_company+status.93dd5488.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead_company+status.93dd5488.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+subject.8d8612d3.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+subject.8d8612d3.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - status + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+status.500ced17.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+status.500ced17.found.yaml + - fields: + - date_start + - direction + - duration_minutes + - lead + - lead_company + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company.9c7e8372.create.yaml + found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company.9c7e8372.found.yaml + - fields: + - date_start + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/date_start+description+direction+duration_minutes+status+subject.2e2c7e7a.create.yaml + found: contracts/log_call/date_start+description+direction+duration_minutes+status+subject.2e2c7e7a.found.yaml + - fields: + - contact + - description + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/contact+description+direction+duration_minutes+status+subject.47ddf296.create.yaml + found: contracts/log_call/contact+description+direction+duration_minutes+status+subject.47ddf296.found.yaml + - fields: + - contact + - date_start + - direction + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/contact+date_start+direction+duration_minutes+status+subject.948cf7ee.create.yaml + found: contracts/log_call/contact+date_start+direction+duration_minutes+status+subject.948cf7ee.found.yaml + - fields: + - contact + - date_start + - description + - duration_minutes + - status + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/contact+date_start+description+duration_minutes+status+subject.9f7503e2.create.yaml + found: contracts/log_call/contact+date_start+description+duration_minutes+status+subject.9f7503e2.found.yaml + - fields: + - contact + - date_start + - description + - direction + - duration_minutes + - subject + lists: {} + dispatch_value: null + create: contracts/log_call/contact+date_start+description+direction+duration_minutes+subject.9f27d6b5.create.yaml + found: contracts/log_call/contact+date_start+description+direction+duration_minutes+subject.9f27d6b5.found.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/report.json b/packages/flow/benchmark/generate_contracts/results/espocrm/report.json new file mode 100644 index 0000000000..a591f68f12 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/report.json @@ -0,0 +1,170 @@ +{ + "applied": [], + "dialect": "mysql", + "tables": 141, + "questions": [ + { + "number": 1, + "key": "bookkeeping", + "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): call_user (1 per write), note (1 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change): action_history_record, auth_log_record, note, note_user Columns that change on their own and are not compared: account: stream_updated_at; contact: stream_updated_at; lead: modified_at, modified_by_id, stream_updated_at Columns of a new record that differ between demonstrations and are not checked: call: parent_type, uid; opportunity: last_stage, probability Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", + "proposal": "accept", + "answered": true + }, + { + "number": 2, + "key": "recheck_after_s", + "text": "How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `.", + "proposal": 300, + "answered": true + } + ], + "kinds": { + "create_contact": { + "mode": "create", + "main_table": "contact", + "locations": { + "account": "contact.account_id -> account.name (exact)", + "account_city": "contact.account_id -> account.billing_address_city (exact)", + "email": "entity_email_address (whose entity_id is the contact key).email_address_id -> email_address.name (exact)", + "first_name": "contact.first_name (exact)", + "last_name": "contact.last_name (exact)", + "phone": "entity_phone_number (whose entity_id is the contact key).phone_number_id -> phone_number.name (phone)", + "title": "account_contact (whose contact_id is the contact key).role (exact)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": { + "account#2": "account_contact", + "account_city#2": "account_contact", + "email": "entity_email_address", + "phone": "entity_phone_number", + "title": "account_contact" + }, + "dispatch_children": {}, + "lists": {}, + "side_rows": [ + { + "table": "note", + "count": 1, + "unless": null + } + ], + "logs": [ + "action_history_record", + "auth_log_record" + ], + "refused": [], + "unresolved": {}, + "emitted": 64, + "rejected": [] + }, + "create_opportunity": { + "mode": "create", + "main_table": "opportunity", + "locations": { + "account": "opportunity.account_id -> account.name (exact)", + "account_city": "opportunity.account_id -> account.billing_address_city (exact)", + "amount": "opportunity.amount (number)", + "close_date": "opportunity.close_date (date)", + "currency": "opportunity.amount_currency (exact)", + "name": "opportunity.name (exact)", + "stage": "opportunity.stage (exact)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": {}, + "dispatch_children": {}, + "lists": {}, + "side_rows": [ + { + "table": "note", + "count": 1, + "unless": null + } + ], + "logs": [ + "action_history_record", + "auth_log_record" + ], + "refused": [], + "unresolved": {}, + "emitted": 64, + "rejected": [] + }, + "update_lead": { + "mode": "update", + "main_table": "lead", + "locations": { + "company": "lead.account_name (exact)", + "first_name": "lead.first_name (exact)", + "last_name": "lead.last_name (exact)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": {}, + "dispatch_children": { + "email": { + "value@email": "entity_email_address" + } + }, + "lists": {}, + "side_rows": [], + "logs": [ + "action_history_record", + "auth_log_record", + "note" + ], + "refused": [], + "unresolved": {}, + "emitted": 43, + "rejected": [] + }, + "log_call": { + "mode": "create", + "main_table": "call", + "locations": { + "account": "call.account_id -> account.name (exact)", + "account_city": "call.account_id -> account.billing_address_city (exact)", + "contact": "call_contact (whose call_id is the call key).contact_id -> contact.first_name || ' ' || last_name (exact)", + "date_start": "call.date_start (datetime)", + "description": "call.description (exact)", + "direction": "call.direction (exact)", + "duration_minutes": "call.date_start || ' ' || date_end (minutes)", + "lead": "call.parent_id -> lead.first_name || ' ' || last_name (exact)", + "lead_company": "call.parent_id -> lead.account_name (exact)", + "status": "call.status (exact)", + "subject": "call.name (exact)" + }, + "placed_by_name": [], + "placed_from_another_kind": [], + "demonstrations": 2, + "children": { + "contact": "call_contact", + "lead#2": "call_lead", + "lead_company#2": "call_lead" + }, + "dispatch_children": {}, + "lists": {}, + "side_rows": [ + { + "table": "call_user", + "count": 1, + "unless": null + } + ], + "logs": [ + "action_history_record", + "auth_log_record", + "note", + "note_user" + ], + "refused": [], + "unresolved": {}, + "emitted": 64, + "rejected": [] + } + } +} \ No newline at end of file From ad5557f6e40cf8f9c3d7f538865223499673e336 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 17:46:26 -0400 Subject: [PATCH 70/94] Versions from an audit trail: narrow only by the record's type --- .../flow/openadapt_flow/generate/verify_contract.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 43bd614da2..904491f9f7 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -971,17 +971,20 @@ def _audit_source( if not pairs or len(pairs) != 1: continue lc, ac, param = next(iter(pairs)) - # Rows of the same type: their constant text and whole-number columns. + # Only a column naming the record's type (``target_type: Lead``) + # narrows the read. Anything else the demonstrations had in common + # (the action, the client address) could hide another user's save, + # so it isn't used: an entry by anyone else stops the write. where = {} + names = {plan.anchor.lower(), plan.anchor.lower().split("_")[-1]} for col in table.columns: - if col.name in (lc, ac) or col.name in table.primary_key: + if col.name in (lc, ac) or col.kind != "text": continue values = {r.get(col.name) for r in rows} if len(values) == 1: v = next(iter(values)) - if v is not None and (col.kind == "text" or _WHOLE_NUMBER.match(v)): - if "\\" not in v and "'" not in v and "%" not in v: - where[col.name] = v + if v is not None and v.lower() in names and v.isidentifier(): + where[col.name] = v found = { "kind": "audit", "table": name, From 759d060ce8f8571d6c863ebba180b6246613090d Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 17:46:52 -0400 Subject: [PATCH 71/94] docs: lost-update versions in generated contracts --- packages/flow/docs/GENERATE_CONTRACTS.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 0b6d28a41c..6013304c66 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -204,6 +204,17 @@ reads, with every column. On PostgreSQL, `frame.database` isn't available, so every other table is read as its own surface; tables proposed as logs are not read there. +**Lost updates.** An update's contracts set `frame.versions`, so another +user's save to the target inside the window stops the write. The generator +takes the versions from the app's own audit trail when every update +demonstration added a row there that names the record and the acting user +(Redmine's journals, EspoCRM's `action_history_record`, Dolibarr's agenda +events); the read is narrowed only by a column naming the record's type, +so any other user's entry for the record counts. Otherwise it uses a whole +number column of the record that went up by one in every demonstration +(Redmine's `lock_version`). With neither, the update has no version check, +and `report.json` shows `versions: null`. + **The recheck window.** On MySQL and MariaDB, one question asks how long the app can still write on its own after a verified write. The answer is `frame.database.recheck_after_s`; the proposal is 300 seconds. From cbe0e0cc7d0b0c4c78d1ca97825f1bee1eec8f34 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 18:11:19 -0400 Subject: [PATCH 72/94] Versions: a column named as a version; the author column when several hold the actor --- ...last_name+phone+title.fef035cb.create.yaml | 339 ++++++++++++++++++ ...+last_name+phone+title.fef035cb.found.yaml | 196 ++++++++++ ...e+currency+name+stage.1d2f2376.create.yaml | 148 ++++++++ ...te+currency+name+stage.1d2f2376.found.yaml | 87 +++++ ...on+duration_minutes+s.dca67171.create.yaml | 175 +++++++++ ...ion+duration_minutes+s.dca67171.found.yaml | 135 +++++++ ...t_name+last_name+value.a43b40a4.found.yaml | 113 ++++++ ..._name+last_name+value.a43b40a4.update.yaml | 160 +++++++++ .../results/espocrm/report.json | 15 +- .../generate/verify_contract.py | 21 +- 10 files changed, 1385 insertions(+), 4 deletions(-) create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml create mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml new file mode 100644 index 0000000000..0a79c06781 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml @@ -0,0 +1,339 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_contact, create, fields account, account_city, email, first_name, last_name, phone, title. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` + AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` + AS CHAR) AS `last_name`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`do_not_call` + AS CHAR) AS `do_not_call`, CAST(t.`address_street` AS CHAR) AS `address_street`, CAST(t.`address_city` + AS CHAR) AS `address_city`, CAST(t.`address_state` AS CHAR) AS `address_state`, CAST(t.`address_country` + AS CHAR) AS `address_country`, CAST(t.`address_postal_code` AS CHAR) AS `address_postal_code`, + CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, + CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, + CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM + `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, + (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) + = CAST(t.`account_id` AS BINARY)) AS `p__account_city`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__deleted`, CASE WHEN t.`do_not_call` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__do_not_call` FROM `contact` t + rows_account_contact: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`contact_id` + AS CHAR) AS `contact_id`, CAST(t.`role` AS CHAR) AS `role`, CAST(t.`is_inactive` AS CHAR) AS `is_inactive`, + CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` + a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` a1 WHERE a1.`id` + = t.`contact_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` AS CHAR) + FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` + a1 WHERE a1.`id` = t.`contact_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` AS CHAR) + FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS + `p__account`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, + CASE WHEN t.`is_inactive` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__is_inactive`, + (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) + = CAST(t.`account_id` AS BINARY)) AS `p__account_city` FROM `account_contact` t + rows_entity_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` + AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS + BINARY)) AS `p__email`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' + END AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__primary` FROM `entity_email_address` t + rows_entity_phone_number: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`phone_number_id` + AS CHAR) AS `phone_number_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `phone_number` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`phone_number_id` AS BINARY)) + AS `p__phone`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, + CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__primary` FROM + `entity_phone_number` t + raw_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`lower` AS CHAR) AS `lower`, CAST(t.`invalid` AS CHAR) AS `invalid`, + CAST(t.`opt_out` AS CHAR) AS `opt_out` FROM `email_address` t + raw_phone_number: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`numeric` AS CHAR) AS `numeric`, + CAST(t.`invalid` AS CHAR) AS `invalid`, CAST(t.`opt_out` AS CHAR) AS `opt_out` FROM `phone_number` + t + raw_note: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`post` + AS CHAR) AS `post`, CAST(t.`data` AS CHAR) AS `data`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`target_type` + AS CHAR) AS `target_type`, CAST(t.`number` AS CHAR) AS `number`, CAST(t.`is_global` AS CHAR) AS + `is_global`, CAST(t.`is_internal` AS CHAR) AS `is_internal`, CAST(t.`is_pinned` AS CHAR) AS `is_pinned`, + CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, + CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` AS CHAR) AS `parent_type`, CAST(t.`related_id` + AS CHAR) AS `related_id`, CAST(t.`related_type` AS CHAR) AS `related_type`, CAST(t.`created_by_id` + AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`super_parent_id` + AS CHAR) AS `super_parent_id`, CAST(t.`super_parent_type` AS CHAR) AS `super_parent_type` FROM + `note` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + first_name: + param: first_name + last_name: + param: last_name + created_by_id: + param: bot_user_id + pin__deleted: + literal: '1' + pin__do_not_call: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds account + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__account: + param: account + pin__deleted: + literal: '1' + pin__is_inactive: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds account_city + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__account_city: + param: account_city + pin__deleted: + literal: '1' + pin__is_inactive: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_entity_email_address|a row beside the main record holds email + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__email: + param: email + kind: email + pin__deleted: + literal: '1' + entity_type: + literal: Contact + pin__primary: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_entity_phone_number|a row beside the main record holds phone + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__phone: + param: phone + kind: phone + pin__deleted: + literal: '1' + entity_type: + literal: Contact + pin__primary: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds title + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + role: + param: title + pin__deleted: + literal: '1' + pin__is_inactive: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_email_address|the new email_address row holds email + match: + name: + param: email + kind: email + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_phone_number|the new phone_number row holds phone + match: + name: + param: phone + kind: phone + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_note|1 row(s) the app adds to note + match: + created_by_id: + param: bot_user_id + deleted: + literal: '0' + is_global: + literal: '0' + is_internal: + literal: '0' + is_pinned: + literal: '0' + parent_type: + literal: Contact + super_parent_type: + literal: Account + type: + literal: Create + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: account_contact + - table: contact + - table: entity_email_address + - table: entity_phone_number + views: + email_address: raw_email_address + phone_number: raw_phone_number + note: raw_note + settle_s: 1 + settle_attempts: 5 + new_row_fields: + email_address: + - deleted + - id + - invalid + - lower + - opt_out + phone_number: + - deleted + - id + - invalid + - numeric + - opt_out + - type + note: + - created_at + - data + - id + - modified_at + - modified_by_id + - number + - parent_id + - post + - related_id + - related_type + - super_parent_id + - target_type + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml new file mode 100644 index 0000000000..5ac58e6a52 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml @@ -0,0 +1,196 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_contact, found, fields account, account_city, email, first_name, last_name, phone, title. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` + AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` + AS CHAR) AS `last_name`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`do_not_call` + AS CHAR) AS `do_not_call`, CAST(t.`address_street` AS CHAR) AS `address_street`, CAST(t.`address_city` + AS CHAR) AS `address_city`, CAST(t.`address_state` AS CHAR) AS `address_state`, CAST(t.`address_country` + AS CHAR) AS `address_country`, CAST(t.`address_postal_code` AS CHAR) AS `address_postal_code`, + CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, + CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, + CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM + `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, + (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) + = CAST(t.`account_id` AS BINARY)) AS `p__account_city` FROM `contact` t + rows_account_contact: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`contact_id` + AS CHAR) AS `contact_id`, CAST(t.`role` AS CHAR) AS `role`, CAST(t.`is_inactive` AS CHAR) AS `is_inactive`, + CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` + a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` a1 WHERE a1.`id` + = t.`contact_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` AS CHAR) + FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` + a1 WHERE a1.`id` = t.`contact_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` AS CHAR) + FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS + `p__account`, (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` + AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account_city` FROM `account_contact` t + rows_entity_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` + AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS + BINARY)) AS `p__email` FROM `entity_email_address` t + rows_entity_phone_number: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`phone_number_id` + AS CHAR) AS `phone_number_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `phone_number` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`phone_number_id` AS BINARY)) + AS `p__phone` FROM `entity_phone_number` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + first_name: + param: first_name + last_name: + param: last_name + expected_count: 1 +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds account + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__account: + param: account + expected_count: 1 +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds account_city + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__account_city: + param: account_city + expected_count: 1 +- kind: record_written + probe: surface=rows_entity_email_address|a row beside the main record holds email + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__email: + param: email + kind: email + expected_count: 1 +- kind: record_written + probe: surface=rows_entity_phone_number|a row beside the main record holds phone + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + p__phone: + param: phone + kind: phone + expected_count: 1 +- kind: record_written + probe: surface=rows_account_contact|a row beside the main record holds title + match: + main__account: + param: account + main__account_city: + param: account_city + main__first_name: + param: first_name + main__last_name: + param: last_name + role: + param: title + expected_count: 1 +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: account_contact + - table: contact + - table: entity_email_address + - table: entity_phone_number + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml new file mode 100644 index 0000000000..02c8105314 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml @@ -0,0 +1,148 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_opportunity, create, fields account, account_city, amount, close_date, currency, name, stage. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`amount` AS CHAR) AS `amount`, CAST(t.`stage` AS CHAR) AS `stage`, + CAST(t.`last_stage` AS CHAR) AS `last_stage`, CAST(t.`probability` AS CHAR) AS `probability`, + CAST(t.`lead_source` AS CHAR) AS `lead_source`, CAST(t.`close_date` AS CHAR) AS `close_date`, + CAST(t.`description` AS CHAR) AS `description`, CAST(t.`created_at` AS CHAR) AS `created_at`, + CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`amount_currency` AS CHAR) AS `amount_currency`, + CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`account_id` AS CHAR) AS `account_id`, + CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` AS CHAR) AS + `version_number`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) + = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` AS CHAR) + FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account_city`, + CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`amount` = CAST(%(v0)s AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__amount`, CASE WHEN t.`close_date` = CAST(%(v1)s AS + DATE) THEN '1' ELSE '0' END AS `p__close_date`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__deleted`, CASE WHEN t.`version_number` = (SELECT s.`version_number` + FROM `account` s WHERE s.`id` = t.`account_id`) THEN '1' ELSE '0' END AS `pin__version_number` + FROM `opportunity` t + sql_query_params: + v0: + param: amount + v1: + param: close_date + raw_note: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`post` + AS CHAR) AS `post`, CAST(t.`data` AS CHAR) AS `data`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`target_type` + AS CHAR) AS `target_type`, CAST(t.`number` AS CHAR) AS `number`, CAST(t.`is_global` AS CHAR) AS + `is_global`, CAST(t.`is_internal` AS CHAR) AS `is_internal`, CAST(t.`is_pinned` AS CHAR) AS `is_pinned`, + CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, + CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` AS CHAR) AS `parent_type`, CAST(t.`related_id` + AS CHAR) AS `related_id`, CAST(t.`related_type` AS CHAR) AS `related_type`, CAST(t.`created_by_id` + AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`super_parent_id` + AS CHAR) AS `super_parent_id`, CAST(t.`super_parent_type` AS CHAR) AS `super_parent_type` FROM + `note` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + p__amount: + literal: '1' + p__close_date: + literal: '1' + amount_currency: + param: currency + name: + param: name + stage: + param: stage + created_by_id: + param: bot_user_id + pin__deleted: + literal: '1' + pin__version_number: + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_note|1 row(s) the app adds to note + match: + created_by_id: + param: bot_user_id + deleted: + literal: '0' + is_global: + literal: '0' + is_internal: + literal: '0' + is_pinned: + literal: '0' + parent_type: + literal: Opportunity + super_parent_type: + literal: Account + type: + literal: Create + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: opportunity + views: + note: raw_note + settle_s: 1 + settle_attempts: 5 + new_row_fields: + note: + - created_at + - data + - id + - modified_at + - modified_by_id + - number + - parent_id + - post + - related_id + - related_type + - super_parent_id + - target_type + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml new file mode 100644 index 0000000000..1942899ebc --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml @@ -0,0 +1,87 @@ +# Generated by openadapt_flow.generate.verify_contract: kind create_opportunity, found, fields account, account_city, amount, close_date, currency, name, stage. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`amount` AS CHAR) AS `amount`, CAST(t.`stage` AS CHAR) AS `stage`, + CAST(t.`last_stage` AS CHAR) AS `last_stage`, CAST(t.`probability` AS CHAR) AS `probability`, + CAST(t.`lead_source` AS CHAR) AS `lead_source`, CAST(t.`close_date` AS CHAR) AS `close_date`, + CAST(t.`description` AS CHAR) AS `description`, CAST(t.`created_at` AS CHAR) AS `created_at`, + CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`amount_currency` AS CHAR) AS `amount_currency`, + CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`account_id` AS CHAR) AS `account_id`, + CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` AS CHAR) AS + `version_number`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) + = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` AS CHAR) + FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account_city`, + CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`amount` = CAST(%(v0)s AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__amount`, CASE WHEN t.`close_date` = CAST(%(v1)s AS + DATE) THEN '1' ELSE '0' END AS `p__close_date` FROM `opportunity` t + sql_query_params: + v0: + param: amount + v1: + param: close_date +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + p__amount: + literal: '1' + p__close_date: + literal: '1' + amount_currency: + param: currency + name: + param: name + stage: + param: stage + expected_count: 1 +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: opportunity + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml new file mode 100644 index 0000000000..b17aa26683 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml @@ -0,0 +1,175 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_call, create, fields account, account_city, contact, date_start, description, direction, duration_minutes, status, subject. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`date_start` AS CHAR) AS `date_start`, + CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`direction` AS CHAR) AS `direction`, CAST(t.`description` + AS CHAR) AS `description`, CAST(t.`uid` AS CHAR) AS `uid`, CAST(t.`created_at` AS CHAR) AS `created_at`, + CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` + AS CHAR) AS `parent_type`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`created_by_id` + AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` + AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` + AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` + AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS + `p__account_city`, CASE WHEN t.`date_start` = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS + `p__date_start`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (TIMESTAMPDIFF(SECOND, + t.`date_start`, t.`date_end`) / 60) = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `p__duration_minutes`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__deleted` FROM `call` t + sql_query_params: + v0: &id002 + param: date_start + v1: &id003 + param: duration_minutes + rows_call_contact: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`call_id` AS CHAR) AS `call_id`, CAST(t.`contact_id` + AS CHAR) AS `contact_id`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`deleted` AS CHAR) AS `deleted`, + (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` + AS BINARY)) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account_city`, CASE WHEN (SELECT a1.`date_start` + FROM `call` a1 WHERE a1.`id` = t.`call_id`) = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS + `main__date_start`, (SELECT CAST(a1.`description` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + AS `main__description`, (SELECT CAST(a1.`direction` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + AS `main__direction`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT + (TIMESTAMPDIFF(SECOND, a1.`date_start`, a1.`date_end`) / 60) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__duration_minutes`, (SELECT CAST(a1.`status` + AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__status`, (SELECT CAST(a1.`name` + AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__subject`, (SELECT CONCAT(CAST(a1.`first_name` + AS CHAR), ' ', CAST(a1.`last_name` AS CHAR)) FROM `contact` a1 WHERE CAST(a1.`id` AS BINARY) = + CAST(t.`contact_id` AS BINARY)) AS `p__contact`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__deleted` FROM `call_contact` t + sql_query_params: + v0: *id002 + v1: *id003 + raw_call_user: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`user_id` AS CHAR) AS `user_id`, CAST(t.`call_id` + AS CHAR) AS `call_id`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`deleted` AS CHAR) AS `deleted` + FROM `call_user` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + p__date_start: + literal: '1' + description: + param: description + direction: + param: direction + p__duration_minutes: + literal: '1' + status: + param: status + name: + param: subject + assigned_user_id: + param: bot_user_id + created_by_id: + param: bot_user_id + pin__deleted: + literal: '1' + parent_type: + literal: Account + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_call_contact|a row beside the main record holds contact + match: + main__account: + param: account + main__account_city: + param: account_city + main__date_start: + literal: '1' + main__description: + param: description + main__direction: + param: direction + main__duration_minutes: + literal: '1' + main__status: + param: status + main__subject: + param: subject + p__contact: + param: contact + pin__deleted: + literal: '1' + status: + literal: None + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=raw_call_user|1 row(s) the app adds to call_user + match: + deleted: + literal: '0' + status: + literal: Accepted + user_id: + param: bot_user_id + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - modified_at + - modified_by_id + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: note + - table: note_user + - table: call + - table: call_contact + views: + call_user: raw_call_user + settle_s: 1 + settle_attempts: 5 + new_row_fields: + call_user: + - call_id + - id + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml new file mode 100644 index 0000000000..6f722304fc --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml @@ -0,0 +1,135 @@ +# Generated by openadapt_flow.generate.verify_contract: kind log_call, found, fields account, account_city, contact, date_start, description, direction, duration_minutes, status, subject. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`date_start` AS CHAR) AS `date_start`, + CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`direction` AS CHAR) AS `direction`, CAST(t.`description` + AS CHAR) AS `description`, CAST(t.`uid` AS CHAR) AS `uid`, CAST(t.`created_at` AS CHAR) AS `created_at`, + CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` + AS CHAR) AS `parent_type`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`created_by_id` + AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` + AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` + AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` + AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS + `p__account_city`, CASE WHEN t.`date_start` = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS + `p__date_start`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (TIMESTAMPDIFF(SECOND, + t.`date_start`, t.`date_end`) / 60) = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `p__duration_minutes` FROM `call` t + sql_query_params: + v0: &id002 + param: date_start + v1: &id003 + param: duration_minutes + rows_call_contact: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`call_id` AS CHAR) AS `call_id`, CAST(t.`contact_id` + AS CHAR) AS `contact_id`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`deleted` AS CHAR) AS `deleted`, + (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` + AS BINARY)) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` + AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM + `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account_city`, CASE WHEN (SELECT a1.`date_start` + FROM `call` a1 WHERE a1.`id` = t.`call_id`) = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS + `main__date_start`, (SELECT CAST(a1.`description` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + AS `main__description`, (SELECT CAST(a1.`direction` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + AS `main__direction`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT + (TIMESTAMPDIFF(SECOND, a1.`date_start`, a1.`date_end`) / 60) FROM `call` a1 WHERE a1.`id` = t.`call_id`) + = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__duration_minutes`, (SELECT CAST(a1.`status` + AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__status`, (SELECT CAST(a1.`name` + AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__subject`, (SELECT CONCAT(CAST(a1.`first_name` + AS CHAR), ' ', CAST(a1.`last_name` AS CHAR)) FROM `contact` a1 WHERE CAST(a1.`id` AS BINARY) = + CAST(t.`contact_id` AS BINARY)) AS `p__contact` FROM `call_contact` t + sql_query_params: + v0: *id002 + v1: *id003 +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + p__account: + param: account + p__account_city: + param: account_city + p__date_start: + literal: '1' + description: + param: description + direction: + param: direction + p__duration_minutes: + literal: '1' + status: + param: status + name: + param: subject + expected_count: 1 +- kind: record_written + probe: surface=rows_call_contact|a row beside the main record holds contact + match: + main__account: + param: account + main__account_city: + param: account_city + main__date_start: + literal: '1' + main__description: + param: description + main__direction: + param: direction + main__duration_minutes: + literal: '1' + main__status: + param: status + main__subject: + param: subject + p__contact: + param: contact + expected_count: 1 +frame: + ignore_fields: + - modified_at + - modified_by_id + target_fields: [] + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: note + - table: note_user + - table: call + - table: call_contact + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + account: + - stream_updated_at + lead: + - modified_at + - modified_by_id + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml new file mode 100644 index 0000000000..0d63d283c1 --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml @@ -0,0 +1,113 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_lead, found, fields company, field, first_name, last_name, value. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` + AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` + AS CHAR) AS `last_name`, CAST(t.`title` AS CHAR) AS `title`, CAST(t.`status` AS CHAR) AS `status`, + CAST(t.`source` AS CHAR) AS `source`, CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`opportunity_amount` + AS CHAR) AS `opportunity_amount`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`address_street` + AS CHAR) AS `address_street`, CAST(t.`address_city` AS CHAR) AS `address_city`, CAST(t.`address_state` + AS CHAR) AS `address_state`, CAST(t.`address_country` AS CHAR) AS `address_country`, CAST(t.`address_postal_code` + AS CHAR) AS `address_postal_code`, CAST(t.`do_not_call` AS CHAR) AS `do_not_call`, CAST(t.`description` + AS CHAR) AS `description`, CAST(t.`converted_at` AS CHAR) AS `converted_at`, CAST(t.`created_at` + AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`account_name` + AS CHAR) AS `account_name`, CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`opportunity_amount_currency` + AS CHAR) AS `opportunity_amount_currency`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_account_id` AS CHAR) AS `created_account_id`, CAST(t.`created_contact_id` AS CHAR) + AS `created_contact_id`, CAST(t.`created_opportunity_id` AS CHAR) AS `created_opportunity_id` + FROM `lead` t + rows_entity_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` + AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT CAST(a1.`account_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__company`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS + BINARY)) AS `p__value` FROM `entity_email_address` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + account_name: + param: company + first_name: + param: first_name + last_name: + param: last_name + expected_count: 1 +- kind: record_written + probe: surface=rows_entity_email_address|a row beside the main record holds value + match: + main__company: + param: company + main__first_name: + param: first_name + main__last_name: + param: last_name + p__value: + param: value + kind: email + expected_count: 1 +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + versions: + read: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`target_id` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`user_id` + AS CHAR) AS `author` FROM `action_history_record` t WHERE CAST(t.`target_type` AS CHAR) = 'Lead' + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: note + - table: entity_email_address + - table: lead + views: {} + settle_s: 1 + settle_attempts: 5 + ignore_columns: + account: + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml new file mode 100644 index 0000000000..8063d931de --- /dev/null +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml @@ -0,0 +1,160 @@ +# Generated by openadapt_flow.generate.verify_contract: kind update_lead, update, fields company, field, first_name, last_name, value. +# Proposed, not admitted: a person answers QUESTIONS.md first. +schema_version: openadapt.verify/v1 +environment_id: generated-espocrm +max_execution_window_s: 900 +verifier: + kind: surfaces + surfaces: + record: + kind: sql + sql_driver: pymysql + sql_connect_args: &id001 + charset: utf8mb4 + database: espocrm + host: 127.0.0.1 + port: 9621 + user: deployer_ro + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` + AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` + AS CHAR) AS `last_name`, CAST(t.`title` AS CHAR) AS `title`, CAST(t.`status` AS CHAR) AS `status`, + CAST(t.`source` AS CHAR) AS `source`, CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`opportunity_amount` + AS CHAR) AS `opportunity_amount`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`address_street` + AS CHAR) AS `address_street`, CAST(t.`address_city` AS CHAR) AS `address_city`, CAST(t.`address_state` + AS CHAR) AS `address_state`, CAST(t.`address_country` AS CHAR) AS `address_country`, CAST(t.`address_postal_code` + AS CHAR) AS `address_postal_code`, CAST(t.`do_not_call` AS CHAR) AS `do_not_call`, CAST(t.`description` + AS CHAR) AS `description`, CAST(t.`converted_at` AS CHAR) AS `converted_at`, CAST(t.`created_at` + AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`account_name` + AS CHAR) AS `account_name`, CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`opportunity_amount_currency` + AS CHAR) AS `opportunity_amount_currency`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, + CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, + CAST(t.`created_account_id` AS CHAR) AS `created_account_id`, CAST(t.`created_contact_id` AS CHAR) + AS `created_contact_id`, CAST(t.`created_opportunity_id` AS CHAR) AS `created_opportunity_id` + FROM `lead` t + rows_entity_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` + AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT CAST(a1.`account_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__company`, (SELECT CAST(a1.`first_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` + AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS + BINARY)) AS `p__value`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' + END AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__primary` FROM `entity_email_address` t + raw_email_address: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`lower` AS CHAR) AS `lower`, CAST(t.`invalid` AS CHAR) AS `invalid`, + CAST(t.`opt_out` AS CHAR) AS `opt_out` FROM `email_address` t +effects: +- kind: record_written + probe: surface=record|the main record holds the task's values + match: + account_name: + param: company + first_name: + param: first_name + last_name: + param: last_name + expected_count: 1 +- kind: record_written + probe: surface=rows_entity_email_address|a row beside the main record holds value + match: + main__company: &id002 + param: company + main__first_name: &id003 + param: first_name + main__last_name: &id004 + param: last_name + p__value: + param: value + kind: email + pin__deleted: &id005 + literal: '1' + entity_type: &id006 + literal: Lead + pin__primary: &id007 + literal: '1' + expected_count: 1 + count_new_only: true +- kind: record_written + probe: surface=rows_entity_email_address|the record's rows here after the write + match: + main__company: *id002 + main__first_name: *id003 + main__last_name: *id004 + pin__deleted: *id005 + entity_type: *id006 + pin__primary: *id007 + expected_count: 1 +- kind: record_written + probe: surface=raw_email_address|the new email_address row holds value + match: + name: + param: value + kind: email + expected_count: 1 + count_new_only: true +frame: + ignore_fields: + - modified_at + - modified_by_id + - stream_updated_at + target_fields: [] + versions: + read: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`target_id` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`user_id` + AS CHAR) AS `author` FROM `action_history_record` t WHERE CAST(t.`target_type` AS CHAR) = 'Lead' + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id + database: + dialect: mysql + read: + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + allow_tables: + - table: action_history_record + - table: auth_log_record + - table: note + - table: entity_email_address + mode: skip + reason: read in full, every column, by this contract's own surface + - table: lead + mode: skip + reason: read in full, every column, by this contract's own surface + views: + email_address: raw_email_address + settle_s: 1 + settle_attempts: 5 + new_row_fields: + email_address: + - deleted + - id + - invalid + - lower + - opt_out + ignore_columns: + account: + - stream_updated_at + contact: + - stream_updated_at + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/report.json b/packages/flow/benchmark/generate_contracts/results/espocrm/report.json index a591f68f12..173dc0a68b 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/report.json +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/report.json @@ -25,13 +25,14 @@ "locations": { "account": "contact.account_id -> account.name (exact)", "account_city": "contact.account_id -> account.billing_address_city (exact)", - "email": "entity_email_address (whose entity_id is the contact key).email_address_id -> email_address.name (exact)", + "email": "entity_email_address (whose entity_id is the contact key).email_address_id -> email_address.name (email)", "first_name": "contact.first_name (exact)", "last_name": "contact.last_name (exact)", "phone": "entity_phone_number (whose entity_id is the contact key).phone_number_id -> phone_number.name (phone)", "title": "account_contact (whose contact_id is the contact key).role (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": { @@ -72,6 +73,7 @@ "stage": "opportunity.stage (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, @@ -102,6 +104,16 @@ "last_name": "lead.last_name (exact)" }, "placed_by_name": [], + "versions": { + "kind": "audit", + "table": "action_history_record", + "record_column": "target_id", + "author_column": "user_id", + "actor_param": "bot_user_id", + "where": { + "target_type": "Lead" + } + }, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, @@ -139,6 +151,7 @@ "subject": "call.name (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": { diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 904491f9f7..f42d88322e 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -915,7 +915,13 @@ def _version_column(schema: Schema, plan: KindPlan) -> Optional[dict[str, Any]]: if len(t.primary_key) != 1: return None for col in t.columns: - if col.kind != "number" or col.name in t.primary_key: + # Named as a version (``lock_version``, ``version_number``): a user + # id that happens to go from 1 to 2 is not one. + if ( + col.kind != "number" + or col.name in t.primary_key + or "version" not in col.name.lower() + ): continue ok = bool(plan.anchors) for a in plan.anchors: @@ -968,9 +974,18 @@ def _audit_source( here.add((lc, ac, p)) rows.append(r) pairs = here if pairs is None else pairs & here - if not pairs or len(pairs) != 1: + if not pairs or len({p[0] for p in pairs}) != 1: continue - lc, ac, param = next(iter(pairs)) + # Several columns hold the acting user (author and assignee): the + # one named as the author, else as a user, else the first by name. + lc, ac, param = sorted( + pairs, + key=lambda p: ( + "author" not in p[1].lower(), + "user" not in p[1].lower(), + p[1], + ), + )[0] # Only a column naming the record's type (``target_type: Lead``) # narrows the read. Anything else the demonstrations had in common # (the action, the client address) could hide another user's save, From 0e01dcc932f7e783f15645bfc21a792f6a49ba6a Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 18:18:59 -0400 Subject: [PATCH 73/94] Lint, types, public artifact inventory, capability status --- CAPABILITIES.md | 2 +- llms-full.txt | 2 +- .../openadapt_flow/generate/demo_analysis.py | 8 +-- .../flow/openadapt_flow/generate/sql_store.py | 10 +-- .../generate/verify_contract.py | 64 ++++++++++--------- packages/flow/public-artifacts.json | 31 +++++++++ .../tests/test_generate_verify_contract.py | 5 +- 7 files changed, 80 insertions(+), 42 deletions(-) diff --git a/CAPABILITIES.md b/CAPABILITIES.md index 04b88ebfba..72477285a5 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,7 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | On main ([#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672)) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/llms-full.txt b/llms-full.txt index d43a2cae5f..e650f69031 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -544,7 +544,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | In review | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | On main ([#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672)) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index eb576f8158..cb3c71cd51 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -120,15 +120,15 @@ def rule_for(stored: Optional[str], column_kind: str, value: Any) -> Optional[st if column_kind == "text": return None if column_kind in ("number", "bool"): - a, b = _decimal(stored), _decimal(text) - if a is not None and b is not None and a == b: + da, db = _decimal(stored), _decimal(text) + if da is not None and db is not None and da == db: return "number" return None if column_kind == "date": return "date" if stored[:10] == text and len(text) == 10 else None if column_kind == "datetime": - a, b = _datetime(stored.split(".")[0]), _datetime(text) - if a is not None and b is not None and a == b: + ta, tb = _datetime(stored.split(".")[0]), _datetime(text) + if ta is not None and tb is not None and ta == tb: return "datetime" return None return None diff --git a/packages/flow/openadapt_flow/generate/sql_store.py b/packages/flow/openadapt_flow/generate/sql_store.py index 7feeede142..db55cab8b8 100644 --- a/packages/flow/openadapt_flow/generate/sql_store.py +++ b/packages/flow/openadapt_flow/generate/sql_store.py @@ -265,9 +265,10 @@ def _mysql_schema(cur: Any) -> Schema: ) ) for tname, cname, cons, rtable, rcol in keys: - t = tables.get(tname) - if t is None: + kt = tables.get(tname) + if kt is None: continue + t = kt if cons == "PRIMARY": t.primary_key.append(str(cname)) elif rtable: @@ -327,9 +328,10 @@ def _pg_schema(cur: Any) -> Schema: (schema,), ) for tname, ctype, cname, rtable, rcol in cons: - t = tables.get(str(tname).split(".")[-1].strip('"')) - if t is None: + ct = tables.get(str(tname).split(".")[-1].strip('"')) + if ct is None: continue + t = ct if ctype == "p": t.primary_key.append(str(cname)) elif rtable and rcol: diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index f42d88322e..f759afda21 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -566,7 +566,7 @@ def digits(v: str) -> str: a, b = other.columns[0], loc.columns[0] for fn in (str.lower, digits): if all( - r.get(b) == (fn(r[a]) if r.get(a) is not None else None) + r.get(b) == (fn(str(r[a])) if r.get(a) is not None else None) for r in rows.rows.values() ) and any(r.get(a) != r.get(b) for r in rows.rows.values()): return True @@ -649,9 +649,9 @@ def _children( todo.append((p, p, loc, None)) for i, x in enumerate(plan.extra.get(p, [])): todo.append((f"{p}#{i + 2}", p, x, None)) - for dv, locs in plan.dispatch_locations.items(): + for value, locs in plan.dispatch_locations.items(): for p, loc in locs.items(): - todo.append((f"{p}@{dv}", p, loc, dv)) + todo.append((f"{p}@{value}", p, loc, value)) for gname, p, loc, dv in todo: if not loc.hops or loc.hops[0].kind != "child": continue @@ -664,7 +664,7 @@ def _children( for d, a in zip(plan.demos, plan.anchors): if d.values.get(p) is None: continue - if dv is not None and str(d.values.get(plan.dispatch)) != dv: + if dv is not None and str(d.values.get(plan.dispatch or "")) != dv: continue for ch in d.diff.of(loc.hops[0].table, "added"): if ch.after is None: @@ -745,17 +745,16 @@ def _children( plan.refused.append(f"{p}: list items must be mappings") return for i, ch in enumerate(children): - if i in used: + if i in used or ch.after is None: continue - locs = { - k: locate( - schema, d, _as_change(ch.table, ch.after), v, max_hops=1 - ) + after = ch.after + found_at = { + k: locate(schema, d, _as_change(ch.table, after), v, max_hops=1) for k, v in item.items() } - if all(locs.values()): + if all(found_at.values()): used.add(i) - link_col = next(c for c, v in ch.after.items() if v == key) + link_col = next(c for c, v in after.items() if v == key) table = ch.table link = Hop( "child", @@ -763,28 +762,28 @@ def _children( link_col, schema.tables[plan.anchor].primary_key[0], ) - for k, ls in locs.items(): + for k, ls in found_at.items(): per_key.setdefault(k, []).append(ls) - rows.append((d, ch.after)) + rows.append((d, after)) break else: plan.unresolved[f"{p} item {item}"] = [] if table is None or link is None: plan.unresolved.setdefault(p, []) continue - locs = {} + item_locs: dict[str, Location] = {} for k, per in per_key.items(): - loc, tied = best(_intersect(per)) - if loc is None: + item_loc, tied = best(_intersect(per)) + if item_loc is None: plan.unresolved[f"{p}.{k}"] = tied return - locs[k] = loc - explained = {link.column} | {c for loc in locs.values() for c in loc.columns} - explained |= {loc.hops[0].column for loc in locs.values() if loc.hops} + item_locs[k] = item_loc + explained = {link.column} | {c for x in item_locs.values() for c in x.columns} + explained |= {x.hops[0].column for x in item_locs.values() if x.hops} plan.lists[p] = ChildGroup( table, link, - locs, + item_locs, column_roles(schema, table, rows, explained, {}), list_param=p, ) @@ -817,9 +816,9 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N added_keys: set[str] = set() for ch in d.diff.changes: if ch.kind == "added" and ch.after is not None: - t = schema.tables.get(ch.table) - if t is not None and len(t.primary_key) == 1: - v = ch.after.get(t.primary_key[0]) + at = schema.tables.get(ch.table) + if at is not None and len(at.primary_key) == 1: + v = ch.after.get(at.primary_key[0]) if v is not None: added_keys.add(v) per_table: dict[str, list[dict[str, Optional[str]]]] = {} @@ -869,15 +868,15 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N continue per_table.setdefault(ch.table, []).append(ch.after or {}) i = plan.demos.index(d) - for t, rows in per_table.items(): + for t, added in per_table.items(): counts.setdefault(t, [0] * len(plan.demos)) - counts[t][i] = max(len(rows), 1 if t in child_tables else 0) - pure.setdefault(t, []).append([(d, r) for r in rows]) + counts[t][i] = max(len(added), 1 if t in child_tables else 0) + pure.setdefault(t, []).append([(d, r) for r in added]) d.__dict__["_added_keys"] = added_keys for t, cs in sorted(counts.items()): rows = [r for group in pure.get(t, []) for r in group] if len(set(cs)) == 1 and cs[0] > 0 and not looks_like_log(t) and rows: - links = [getattr(d, "_added_keys", set()) for d, _ in rows] + links: list[set[str]] = [getattr(d, "_added_keys", set()) for d, _ in rows] roles = column_roles(schema, t, rows, set(), actor_values, links) plan.sides.append(SideSpec(t, cs[0], roles, unless=child_tables.get(t))) elif rows: @@ -1493,7 +1492,7 @@ def build_contract( name = f"rows:{g.table}" sname = _alias(name) if sname not in surfaces: - cols = _base_columns(b, g.table) + scols = _base_columns(b, g.table) link = g.link # The main record's values, seen from the child row. for ip in identity_params: @@ -1506,8 +1505,10 @@ def build_contract( expr = b.path("t", g.table, up) if loc.rule in ("number", "date", "datetime", "minutes"): expr = b.flag(expr, loc.rule, b.ph(ip)) - cols.append((f"main__{_alias(ip)}", expr)) - surfaces[sname] = Surface(sname, settings.views.get(g.table, g.table), cols) + scols.append((f"main__{_alias(ip)}", expr)) + surfaces[sname] = Surface( + sname, settings.views.get(g.table, g.table), scols + ) named_tables.add(g.table) surf = surfaces[sname] items = range(variant.lists[p]) if g.list_param else [None] @@ -2105,7 +2106,8 @@ def dry_read(path: Path, params: Mapping[str, Any], mysql: bool) -> Optional[str ) from openadapt_flow.verify_run import _audit_sql, _db_connect, _ref_columns - database = contract.config.frame.database + frame = contract.config.frame + database: Any = frame.database if frame is not None else None try: take_snapshot( _db_connect(database.read), diff --git a/packages/flow/public-artifacts.json b/packages/flow/public-artifacts.json index 023180f5a1..78f8e1a024 100644 --- a/packages/flow/public-artifacts.json +++ b/packages/flow/public-artifacts.json @@ -272,6 +272,37 @@ "benchmark/frappe_lending/environment.lock.json", "benchmark/gate_per_class/results/after.json", "benchmark/gate_per_class/results/before.json", + "benchmark/generate_contracts/results/dolibarr/answers.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml", + "benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml", + "benchmark/generate_contracts/results/dolibarr/manifest.yaml", + "benchmark/generate_contracts/results/dolibarr/report.json", + "benchmark/generate_contracts/results/espocrm/answers.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml", + "benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml", + "benchmark/generate_contracts/results/espocrm/manifest.yaml", + "benchmark/generate_contracts/results/espocrm/report.json", + "benchmark/generate_contracts/results/redmine/answers.yaml", + "benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml", + "benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml", + "benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml", + "benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml", + "benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml", + "benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml", + "benchmark/generate_contracts/results/redmine/manifest.yaml", + "benchmark/generate_contracts/results/redmine/report.json", "benchmark/governed_run/results.json", "benchmark/grounding_eval/results.json", "benchmark/grounding_eval/results_sweep.json", diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index 1d5b5aec92..62247e50d9 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -382,7 +382,10 @@ def test_an_update_already_in_place_with_no_write_is_confirmed( app = generated["app"] v = {"project": "Help Desk", "subject": "VPN drops", "status": None, "assignee": None, "due_date": None, "done_ratio": 80, "notes": None} # fmt: skip - app.q("UPDATE issues SET done_ratio=80 WHERE id=%s", (app.find("Help Desk", "VPN drops"),)) + app.q( + "UPDATE issues SET done_ratio=80 WHERE id=%s", + (app.find("Help Desk", "VPN drops"),), + ) assert _run(generated, "update_issue", v, lambda: None).startswith("CONFIRMED") # The same task with a stray write on the issue is stopped. stray = _run(generated, "update_issue", v, From f3008fae9ef2356314bc00a7a4f9098c7c720a9d Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 18:45:20 -0400 Subject: [PATCH 74/94] Final real-app results on the rebased branch --- .../benchmark/generate_contracts/RESULTS.md | 26 +++++++++++++------ ...w_email+new_phone+town.110f7554.found.yaml | 14 ++++++++++ ..._email+new_phone+town.110f7554.update.yaml | 14 ++++++++++ .../results/dolibarr/report.json | 11 ++++++++ ...project+status+subject.4aa4d777.found.yaml | 14 ++++++++++ ...roject+status+subject.4aa4d777.update.yaml | 14 ++++++++++ .../results/redmine/report.json | 11 ++++++++ 7 files changed, 96 insertions(+), 8 deletions(-) diff --git a/packages/flow/benchmark/generate_contracts/RESULTS.md b/packages/flow/benchmark/generate_contracts/RESULTS.md index 4c0c0e5817..b686b7dda2 100644 --- a/packages/flow/benchmark/generate_contracts/RESULTS.md +++ b/packages/flow/benchmark/generate_contracts/RESULTS.md @@ -25,11 +25,12 @@ Late-write faults also ran the recheck the contract declares. | Questions for a person | **3** | **1** | **2** | | First study, person decisions | 11 | 11 | 11 | | Contracts written | 93 | 95 | 235 | -| Correct writes confirmed (tasks and controls) | 40/43 | 35/38 | 35/40 | +| Correct writes confirmed (tasks and controls) | 40/43 | 35/38 | 36/40 | | In-scope planted faults stopped | **22/22** | **19/19** | **21/21** | | First study, in-scope faults stopped | 21/21 | 26/26 | 16/19 | | Out-of-scope late writes stopped | 2/2 (recheck) | 0/2 (no recheck on PostgreSQL) | 1/2 (recheck) | -| `propose` time | 1.5 min | 2.5 min | 2 min | +| Lost-update versions (`frame.versions`) | agenda events (`llx_actioncomm`) | journals | `action_history_record` | +| `propose` time | 1.5 min | 1.5 min | 2 min | The first study measured false stops on agents' writes; here the correct writes are the pack's reference writes and the controls, so the two false @@ -53,7 +54,10 @@ Every answer was the proposal, except the one choice in Dolibarr's question 3. - Dolibarr and EspoCRM: `GRANT TRIGGER ON .* TO ` (`--grant-trigger`): the whole-database frame must see triggers. -- EspoCRM before the SQL guard fix (#1665): a view `espocrm_oa_read.oa_read_call` (`--create-views`), because the guard refused the table name `call`. After the rebase the guard reads `call` and no view is needed. +- Before the SQL guard fix (#1665), EspoCRM also needed a view + `espocrm_oa_read.oa_read_call` (`--create-views`), because the guard + refused the table name `call`. On `main` the guard reads `call`, and the + final run created no view. ### False stops, by cause @@ -68,12 +72,15 @@ Every answer was the proposal, except the one choice in Dolibarr's question 3. | EspoCRM | T21 | The reference write removes the old email address row when replacing it; the demonstration didn't | | EspoCRM | T22 | `field: phone` has no contract: no demonstration, and phones live in another table | | EspoCRM | F22, F27 (controls) | Another user's unrelated edit or new lead: not attributed | -| EspoCRM | F24 (control) | Mixed-case email and padded name (emails now compare case-folded) | + +The lost-update faults are stopped with a version reason now (EspoCRM F19: +"another user saved a version of the target inside the window"); the first +study missed EspoCRM F19 and HAPI F14. ### Misses Out of scope only: Redmine F19 and F20 (late writes; PostgreSQL has no -recheck). EspoCRM F21 (a late edit to a look-alike lead: an update's main table is read by its own surface, which the recheck doesn't read). No in-scope planted fault was confirmed on any app. +recheck), and EspoCRM F21 (a late edit to a look-alike lead: an update's main table is read by its own surface, which the recheck doesn't read). No in-scope planted fault was confirmed on any app. ## Reproduce @@ -93,6 +100,9 @@ python summarize.py dolibarr WORK full set is regenerated by the commands above). The fault scripts and the oracle stay in the study's private pack. -Code: these runs used this branch before the rebase onto `main`, with #1661 -(the whole-database frame fix) merged in, and before `frame.versions` was -emitted. Reruns on the rebased branch are in progress. +Code: the final runs used this branch rebased on `main` (with #1661 to +#1669): EspoCRM at `baf86e78a`, Dolibarr and Redmine at `c51437ba6` (the +commits after are lint, type and documentation fixes). Earlier runs on the +branch before the rebase, with #1661 merged in, gave the same counts for +Dolibarr and Redmine; for EspoCRM they missed nothing more and confirmed +one write fewer (F24, before emails compared case-folded). diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml index 6b5b5cbc46..9dee225bd5 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml @@ -84,6 +84,20 @@ frame: - socialnetworks - tms target_fields: [] + versions: + read: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`fk_soc` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`fk_user_author` + AS CHAR) AS `author` FROM `llx_actioncomm` t + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id database: dialect: mysql read: diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml index ac485d5716..8355de323a 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml @@ -165,6 +165,20 @@ frame: target_fields: - email - phone + versions: + read: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: FCG_DOLIBARR_READER_PASSWORD + sql_query: SELECT CAST(t.`fk_soc` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`fk_user_author` + AS CHAR) AS `author` FROM `llx_actioncomm` t + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id database: dialect: mysql read: diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json index 916e1bc81e..4ad05e82bf 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json @@ -39,6 +39,7 @@ "zip": "llx_societe.zip (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, @@ -72,6 +73,7 @@ "vat_rate": "llx_product.tva_tx (number)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": { @@ -107,6 +109,7 @@ "town": "llx_facture.fk_soc -> llx_societe.town (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, @@ -136,6 +139,14 @@ "new_address", "new_zip" ], + "versions": { + "kind": "audit", + "table": "llx_actioncomm", + "record_column": "fk_soc", + "author_column": "fk_user_author", + "actor_param": "bot_user_id", + "where": {} + }, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml index fc5336b4d8..95a96cf13c 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml @@ -574,3 +574,17 @@ frame: - others - updated_on target_fields: [] + versions: + read: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."journalized_id" AS TEXT) AS "id", CAST(t."id" AS TEXT) AS "ver", CAST(t."user_id" + AS TEXT) AS "author" FROM "journals" t + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml index 3c41dbe83f..295dcbaf95 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml @@ -588,3 +588,17 @@ frame: target_fields: - p__status - status_id + versions: + read: + kind: sql + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + sql_query: SELECT CAST(t."journalized_id" AS TEXT) AS "id", CAST(t."id" AS TEXT) AS "ver", CAST(t."user_id" + AS TEXT) AS "author" FROM "journals" t + record_field: id + target_field: id + version_field: ver + author_field: author + self: + - param: bot_user_id diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/report.json b/packages/flow/benchmark/generate_contracts/results/redmine/report.json index 188f7efd34..fc31d7963c 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/report.json +++ b/packages/flow/benchmark/generate_contracts/results/redmine/report.json @@ -29,6 +29,7 @@ "placed_by_name": [ "description" ], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": { @@ -60,6 +61,14 @@ "placed_by_name": [ "done_ratio" ], + "versions": { + "kind": "audit", + "table": "journals", + "record_column": "journalized_id", + "author_column": "user_id", + "actor_param": "bot_user_id", + "where": {} + }, "placed_from_another_kind": [ "due_date", "priority" @@ -91,6 +100,7 @@ "spent_on": "time_entries.spent_on (date)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, @@ -114,6 +124,7 @@ "tracker": "issues.tracker_id -> trackers.name (exact)" }, "placed_by_name": [], + "versions": null, "placed_from_another_kind": [], "demonstrations": 2, "children": {}, From bd267c94dcbc226e8bc8abeda70227d7b9969f7a Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 19:12:18 -0400 Subject: [PATCH 75/94] Round 1 review: confirmed phone rule, row-scoped bookkeeping, found pins constants, unique live display values, identity-only child links, no name-based logs, write-event version sources, quoted grants; docs --- CAPABILITIES.md | 2 +- llms-full.txt | 2 +- packages/flow/docs/GENERATE_CONTRACTS.md | 111 ++-- .../openadapt_flow/generate/demo_analysis.py | 38 +- .../generate/verify_contract.py | 590 +++++++++++++++--- .../tests/test_generate_verify_contract.py | 5 +- .../test_generate_verify_contract_attacks.py | 263 ++++++++ 7 files changed, 856 insertions(+), 155 deletions(-) create mode 100644 packages/flow/tests/test_generate_verify_contract_attacks.py diff --git a/CAPABILITIES.md b/CAPABILITIES.md index 72477285a5..e87ee02613 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,7 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | On main ([#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672)) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/llms-full.txt b/llms-full.txt index e650f69031..a0cd53cf76 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -544,7 +544,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | On main ([#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672)) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 6013304c66..db9d014833 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -119,12 +119,19 @@ that as a stop: the write must not be sent unchecked. | Rule | Example | | --- | --- | | The stored text equals the value | `subject` | -| Equal after case folding, for any value shaped like an email address | `Ana@Example.org` | -| Equal digits, for any value shaped like a phone number | `02 38 53 21 09` stored as `0238532109` | +| Equal after case folding, for a value shaped like an email address | `Ana@Example.org` | +| Equal digits, only for a parameter a person confirmed as a phone number (the `phones` question), when the app stored other text with the same digits | `02 38 53 21 09` stored as `0238532109` | | Equal as a number, a date or a date-time, compared in SQL | `9.20` stored as `9.2`; `2026-10-27 14:30` stored as `2026-10-27 14:30:00` | | Minutes between two date-time columns of the row | a call's duration | | A display column of the row a key column points to | `project_id` -> `projects.name` | | Two text columns joined by one space | `firstname` and `lastname` | + +A display value behind a key must name exactly one row of its table that +isn't soft-deleted (a `deleted` or `is_deleted` flag, or a `deleted_at` +time): a second "Jon Park", or "Mary Ann Lee" split as `Mary Ann|Lee` and +`Mary|Ann Lee`, makes the value empty, so the write stops. At run time a +confirmed phone value that isn't written like a phone number (fewer than 7 +digits, or other characters) is compared exactly. | A row the write added that holds the main record's key | a journal note, a custom field value | | A row it added that links the record to the row holding the value | an email address, a call's attendee | @@ -169,51 +176,69 @@ too. A list parameter gets a contract per length, from 1 to 10 items. - `create`: exactly one new main record holds the task's values, and everything else in the new row is what the demonstrations showed, or is bookkeeping. -- `found`: the record already holds the task's values, and nothing in the - database changed. `choose` reads the store first and picks `found` when a - record with the task's values is already there: a "make sure it exists" - task whose record exists, or an update that is already in place. +- `found`: the record already holds the task's values and, for a create, + the same constants the demonstrations' new rows held (a soft-delete flag, a + status, a type), and nothing in the database changed. `choose` reads the + store first and picks `found` when such a record is already there: a "make + sure it exists" task whose record exists, or an update that is already in + place. A trashed record with the task's values is not found. - `update`: exactly one record matches the task's identity values and holds - the new values. Only the fields the task changes may change. When the + the new values. Only the fields the task changes may change, plus the + target's own bookkeeping columns a person confirmed. When the demonstrations replaced the record's rows beside it (an old email address link removed), the earlier rows may go, and afterwards the record has as many such rows as the demonstrations left. -**Columns of a new row no value explains.** A key, a time stamp from the -write, the acting user, a link to a row the write added, the year, month, -week or day of a date the task gives, a generated token (a UUID), or a copy -of a column of the row a key points to (an invoice line's price from its -product) is recognized as such. Free text the demonstrations left empty is -not checked. Any other column must hold the value the demonstrations -showed; one that differed between them is not checked and is listed in the -bookkeeping question. +**Columns of a new row no value explains.** Each is pinned to the acting +user, to the value every demonstration held, or to a copy of a column of the +row a key points to (an invoice line's price from its product). A key, a +link to a row the write added, the year, month, week or day of a date the +task gives, and a generated token (a UUID) are recognized as such. Columns +that differ between demonstrations, free text every demonstration left +empty, and the write's time stamps are not checked; the bookkeeping question +lists each of them by table, so a person sees every column left open. **Rows the app adds on its own.** A row type the app added the same number of times in every demonstration (an agenda event, a stream note, a price-history row) is declared with that count and its constant columns. A table whose -added rows vary between demonstrations, or whose name says log, history, -audit or similar, is proposed as a log of that kind. Columns that changed -with no task value (a modified time, a version counter) are proposed as -bookkeeping. All of it goes in one question. +added rows differ in number between demonstrations is listed in the +bookkeeping question and is a log only if the person names it (`logs_add`); +otherwise every added row stops the write. The generator never decides a +table is a log from its name. + +**Changes the app makes to existing rows.** A column of an existing row that +the demonstrations changed with no task value is excused only on that row: +the target itself, a row with the same key in every demonstration (a counter +row), or the row the task's values name through a key of the main record +(the account a contact belongs to). It is never excused by name on other +rows or tables. Time stamps set during the write, counters that went up and +columns set to the acting user are proposed; anything else is listed and +excused only if the person adds it (`columns_add`). A changed row the +generator can't identify is listed too, and a write that changes it stops. **The frame.** On MySQL and MariaDB every contract uses the whole-database frame (`frame.database`): a row added, removed or changed in any table that -the contract doesn't declare stops the write. The main record's table and -the tables of rows that hold values are read in full by the contract's own -reads, with every column. On PostgreSQL, `frame.database` isn't available, so -every other table is read as its own surface; tables proposed as logs are -not read there. +the contract doesn't declare stops the write, except rows added to a table +the person named as a log. The main record's table and the tables of rows +that hold values are read by the contract's own reads, with every column. +On PostgreSQL, `frame.database` isn't available, so every other table is read +as its own surface, except the tables the person named as logs, which are +not read at all. **Lost updates.** An update's contracts set `frame.versions`, so another user's save to the target inside the window stops the write. The generator takes the versions from the app's own audit trail when every update demonstration added a row there that names the record and the acting user (Redmine's journals, EspoCRM's `action_history_record`, Dolibarr's agenda -events); the read is narrowed only by a column naming the record's type, -so any other user's entry for the record counts. Otherwise it uses a whole -number column of the record that went up by one in every demonstration -(Redmine's `lock_version`). With neither, the update has no version check, -and `report.json` shows `versions: null`. +events). The read is narrowed to the record's type and to write events: a +column whose value in the update rows differs from its value in the rows +creates added to the same table (EspoCRM's `action`, Dolibarr's `code`), so +another user only viewing the record doesn't count. Otherwise it uses a +whole-number column named as a version that went up by one in every +demonstration (Redmine's `lock_version`). With neither, the update has no +version check, and `report.json` shows `versions: null`. If the app stops +writing the trail (an agenda setting turned off), the agent's own save has +no new version, and the verifier stops the write rather than pass it. **The recheck window.** On MySQL and MariaDB, one question asks how long the app can still write on its own after a verified write. The answer is @@ -226,10 +251,12 @@ that needs something the store doesn't have is either set up, with a flag and an admin connection you give, or not written, with the reason. - `--grant-trigger --admin-read admin.yaml --reader-account "'oa_reader'@'%'"`: - grants `TRIGGER` on the database to the read account (MySQL, MariaDB). + grants `TRIGGER` on the database to the read account (MySQL, MariaDB). The + account name is split into user and host and escaped by the driver. Without it, the generator stops and prints the `GRANT` statement for the - DBA. Note that `TRIGGER` also lets the account create triggers. The verifier - needs it only to see them. + DBA. `TRIGGER` also lets the account create and drop triggers; the verifier + needs it only to see them, and `report.json` says so under `read_account`. + Keep that account's password away from the agent. - `--create-views --admin-read admin.yaml --reader-account ...`: the verifier's SQL guard refuses some table names in a read (EspoCRM's `call`). This creates a view `oa_read_
` in a schema of its own, @@ -252,18 +279,18 @@ why. - A list item that appears twice in one task (the same product and quantity twice) is stopped: each item must match one new row. - Columns of a new record that differ between demonstrations and aren't - task values (an invoice's computed totals, its draft reference) are not - checked. The bookkeeping question lists them. -- Free-text columns of a new record that the task doesn't give, and that - every demonstration left empty (a line description, a note), are not - checked: an agent may fill them from the record it copies. + task values (an invoice's computed totals, its due date), free text every + demonstration left empty, and time stamps are not checked. The bookkeeping + question lists every one of them. - With one demonstration of a create, every other column must hold the value it held then, including one computed from the task's values (a price with tax). `QUESTIONS.md` says so; record a second create with other values. -- On PostgreSQL there is no whole-database frame and no recheck: tables - proposed as logs are not read, and a write that lands after the check is - not seen. +- On PostgreSQL there is no whole-database frame, so there is no recheck (a + write that lands after the check is not seen), and a table the person names + as a log is not read at all: a rewrite of its existing rows passes. Every + other table is read and compared. - REST metadata (OpenAPI) is not read yet. Apps without database access need hand-written contracts. -- The generator does not read an audit trail, so another user's edit during - the window stops the write instead of being attributed to them. +- The frame doesn't attribute changes to other users: another user's edit to + an unrelated record during the window stops the write. Only the target's + own versions are read from the audit trail (lost updates). diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index cb3c71cd51..6b37d78234 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -49,12 +49,19 @@ _IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") _EMAIL = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") _PHONE = re.compile(r"^\+?[0-9][0-9 ().\-/]*$") -#: Table names an app's own logs usually carry. Only a proposal: a person -#: confirms which tables are logs (the bookkeeping question). -_LOG_NAME = re.compile( - r"(log|history|audit|session|token|notification|queue|job|stat|cache|lock|event)", - re.I, -) +#: Fewest digits a value compared as a phone number may have. +PHONE_MIN_DIGITS = 7 + + +def phone_shaped(value: Any) -> bool: + """Is ``value`` written like a phone number (digits and ``+ ( ) - . /`` + and spaces, with at least :data:`PHONE_MIN_DIGITS` digits)?""" + + text = value_text(value) if value is not None else "" + return ( + bool(_PHONE.match(text)) + and sum(ch.isdigit() for ch in text) >= PHONE_MIN_DIGITS + ) def value_text(value: Any) -> str: @@ -102,13 +109,18 @@ def rule_for(stored: Optional[str], column_kind: str, value: Any) -> Optional[st if text == "": return None if column_kind == "text": - # An email address or a phone number is compared in its declared - # canonical form even when this demonstration's text matches - # exactly: another write may store ``Ana@Example.org`` or - # ``+16175550144`` for the same value. + # An email address is compared case-folded even when this + # demonstration's text matches exactly (another write may store + # ``Ana@Example.org``). A phone number only when the app stored other + # text with the same digits, and only as a proposal: a person + # confirms it (the ``phones`` question), never the value's shape. if _EMAIL.match(text) and stored.casefold() == text.casefold(): return "email" - if _PHONE.match(text) and sum(ch.isdigit() for ch in text) >= 6: + if ( + stored != text + and _PHONE.match(text) + and sum(ch.isdigit() for ch in text) >= PHONE_MIN_DIGITS + ): a, b = kind_form("phone", stored), kind_form("phone", text) if a is not None and a == b: return "phone" @@ -516,10 +528,6 @@ def snake(name: str) -> str: return re.sub(r"(? bool: - return bool(_LOG_NAME.search(table)) - - @dataclass class ColumnRole: """What a column of a new row is, when no parameter explains it.""" diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index f759afda21..70e9636752 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -58,7 +58,6 @@ is_identifier, link_columns, locate, - looks_like_log, pick_anchor, ) from openadapt_flow.generate.sql_store import RowChange, Schema @@ -249,6 +248,18 @@ class KindPlan: by_name: list[str] = field(default_factory=list) #: Parameters placed where another kind's demonstrations found them. shared: list[str] = field(default_factory=list) + #: Existing rows (not the target) the demonstrations changed with no + #: task value: each the same row every time (by key) or the row a + #: located value names (an account behind the contact's key), with the + #: columns that moved and what kind of change each was. + touched: list[dict[str, Any]] = field(default_factory=list) + #: The target's own columns that moved with no task value, by kind. + anchor_extra: dict[str, str] = field(default_factory=dict) + #: Changed rows nothing identifies: a write that changes them stops. + unexcused: dict[str, set[str]] = field(default_factory=dict) + #: Tables whose added rows differ between demonstrations: logs only if a + #: person says so. + log_candidates: set[str] = field(default_factory=set) #: Where the target's versions are read (``frame.versions``): a version #: column that steps by one per save, or an audit table naming the #: record and the user who saved it. None when the demonstrations show @@ -451,7 +462,7 @@ def analyze( "target can't be told apart from other records" ) _children(schema, plan, lists) - _sides(schema, plan, actor_values) + _sides(schema, plan, actor_values, demos) return plan @@ -573,6 +584,32 @@ def digits(v: str) -> str: return False +def _all_locations(plan: KindPlan) -> list[tuple[str, Location]]: + out = list(plan.locations.items()) + for locs in plan.dispatch_locations.values(): + out.extend(locs.items()) + for g in [*plan.children.values(), *plan.lists.values()]: + out.extend(g.params.items()) + return out + + +def _confirm_phones(plan: KindPlan, confirmed: set[str]) -> None: + """A phone rule a person didn't confirm compares the text exactly.""" + + def fix(name: str, loc: Location) -> Location: + if loc.rule == "phone" and name not in confirmed: + return Location(loc.hops, loc.columns, "exact") + return loc + + plan.locations = {k: fix(k, v) for k, v in plan.locations.items()} + plan.dispatch_locations = { + dv: {k: fix(k, v) for k, v in locs.items()} + for dv, locs in plan.dispatch_locations.items() + } + for g in [*plan.children.values(), *plan.lists.values()]: + g.params = {k: fix(k, v) for k, v in g.params.items()} + + def _resolve(plan: KindPlan, key: str, loc: Location) -> None: """Place an unresolved value (answered, or settled by a tie-break).""" @@ -805,13 +842,19 @@ def _demo_groups(plan: KindPlan, d: Demo) -> list[ChildGroup]: return _all_groups(plan, dv) if dv is not None else list(plan.children.values()) -def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> None: +def _sides( + schema: Schema, + plan: KindPlan, + actor_values: Mapping[str, str], + all_demos: Sequence[Demo] = (), +) -> None: """Classify every other row the demonstrations touched.""" child_tables = {g.table: g.param for g in _all_groups(plan)} list_tables = {g.table for g in plan.lists.values()} pure: dict[str, list[list[tuple[Demo, dict[str, Optional[str]]]]]] = {} counts: dict[str, list[int]] = {} + touched: list[tuple[Demo, RowChange, RowChange]] = [] for d, a in zip(plan.demos, plan.anchors): added_keys: set[str] = set() for ch in d.diff.changes: @@ -846,14 +889,8 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N g.changed_columns = sorted( set(g.changed_columns) | set(ch.changed_columns) ) - elif ch.table == plan.anchor: - plan.bookkeeping.setdefault(ch.table, set()).update( - ch.changed_columns - ) - else: - plan.bookkeeping.setdefault(ch.table, set()).update( - ch.changed_columns - ) + elif ch.table != plan.anchor or ch.key != a.key: + touched.append((d, a, ch)) continue # added if ch.table in list_tables: @@ -875,12 +912,16 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N d.__dict__["_added_keys"] = added_keys for t, cs in sorted(counts.items()): rows = [r for group in pure.get(t, []) for r in group] - if len(set(cs)) == 1 and cs[0] > 0 and not looks_like_log(t) and rows: + if len(set(cs)) == 1 and cs[0] > 0 and rows: links: list[set[str]] = [getattr(d, "_added_keys", set()) for d, _ in rows] roles = column_roles(schema, t, rows, set(), actor_values, links) plan.sides.append(SideSpec(t, cs[0], roles, unless=child_tables.get(t))) elif rows: - plan.logs.add(t) + # The number of added rows differs between demonstrations: a log + # only when a person says so (bookkeeping question). Until then + # every added row stops the write. + plan.log_candidates.add(t) + _touched_rows(schema, plan, touched, actor_values) if plan.mode == "update": for a in plan.anchors: plan.anchor_changed |= set(a.changed_columns) @@ -896,15 +937,145 @@ def _sides(schema: Schema, plan: KindPlan, actor_values: Mapping[str, str]) -> N for loc in locs.values(): explained |= set(loc.columns) if not loc.hops else {loc.hops[0].column} extra = plan.anchor_changed - explained - if extra: - plan.bookkeeping.setdefault(plan.anchor, set()).update(extra) - plan.versions = _audit_source(schema, plan, actor_values) or _version_column( - schema, plan - ) + for col in sorted(extra): + plan.anchor_extra[col] = _change_kind( + schema, plan.anchor, col, plan.demos, plan.anchors, actor_values + ) + plan.versions = _audit_source( + schema, plan, actor_values, all_demos + ) or _version_column(schema, plan) _WHOLE_NUMBER = re.compile(r"^-?[0-9]+$") +#: Kinds of change a person is offered as the app's own bookkeeping by +#: default. Anything else is listed but not excused unless they add it. +BOOKKEEPING_KINDS = ("timestamp", "counter", "actor") + + +def _change_kind( + schema: Schema, + table: str, + col: str, + demos: Sequence[Demo], + changes: Sequence[RowChange], + actor_values: Mapping[str, str], +) -> str: + """``timestamp`` (set to a time inside the demonstration), ``counter`` + (a whole number that went up), ``actor`` (set to the acting user) or + ``other``, the same in every demonstration that changed it.""" + + from openadapt_flow.generate.demo_analysis import _within + + column = schema.tables[table].column(col) + kinds = set() + for d, ch in zip(demos, changes): + before = (ch.before or {}).get(col) + after = (ch.after or {}).get(col) + if before == after: + continue + if ( + column is not None + and column.kind in ("date", "datetime") + and _within(after, d.started_at, d.finished_at) + ): + kinds.add("timestamp") + elif ( + column is not None + and column.kind in ("number", "text") + and before is not None + and after is not None + and _WHOLE_NUMBER.match(before) + and _WHOLE_NUMBER.match(after) + and int(after) > int(before) + ): + kinds.add("counter") + elif after is not None and after in set(actor_values.values()): + kinds.add("actor") + else: + kinds.add("other") + return kinds.pop() if len(kinds) == 1 else "other" + + +def _touched_rows( + schema: Schema, + plan: KindPlan, + touched: Sequence[tuple[Demo, RowChange, RowChange]], + actor_values: Mapping[str, str], +) -> None: + """Identify each existing row a demonstration changed with no task + value, so an excuse applies to that row only: the same row (by key) in + every demonstration (a counter row), or the row a located value names + through a key of the main record (the contact's account).""" + + by_table: dict[str, list[tuple[Demo, RowChange, RowChange]]] = {} + for item in touched: + by_table.setdefault(item[2].table, []).append(item) + for table, items in sorted(by_table.items()): + t = schema.tables.get(table) + cols = sorted({c for _, _, ch in items for c in ch.changed_columns}) + if t is None or len(t.primary_key) != 1: + plan.unexcused.setdefault(table, set()).update(cols) + continue + pk = t.primary_key[0] + keys = {ch.key for _, _, ch in items} + demos_with = {id(d) for d, _, _ in items} + selector: Optional[dict[str, Any]] = None + how = "" + if ( + len(keys) == 1 + and len(demos_with) == len(plan.demos) + and len(items) == len(plan.demos) + ): + key = (items[0][2].after or {}).get(pk) + if key is not None: + selector = {pk: {"literal": key}} + how = f"{table} row {pk}={key}" + if selector is None: + # Reached from the main record by a key column whose target row + # holds located values in single columns. + for p, loc in plan.locations.items(): + if ( + not loc.hops + or loc.hops[0].kind != "fk" + or loc.hops[0].table != table + ): + continue + hop = loc.hops[0] + if all( + (a.after or {}).get(hop.column) == (ch.after or {}).get(pk) + for _, a, ch in items + ): + sel = { + x.columns[0]: {"param": q} + for q, x in plan.locations.items() + if len(x.hops) == 1 + and x.hops[0] == hop + and len(x.columns) == 1 + and x.rule == "exact" + } + if sel: + selector = sel + how = f"the {table} row the task's {', '.join(sorted(v['param'] for v in sel.values()))} name" + break + if selector is None or len(items) != len({id(d) for d, _, _ in items}): + plan.unexcused.setdefault(table, set()).update(cols) + continue + kinds = { + c: _change_kind( + schema, + table, + c, + [d for d, _, _ in items], + [ch for _, _, ch in items], + actor_values, + ) + for c in cols + } + plan.touched.append( + {"table": table, "selector": selector, "columns": kinds, "row": how} + ) + def _version_column(schema: Schema, plan: KindPlan) -> Optional[dict[str, Any]]: """A whole-number column of the main record that went up by exactly one @@ -941,7 +1112,10 @@ def _version_column(schema: Schema, plan: KindPlan) -> Optional[dict[str, Any]]: def _audit_source( - schema: Schema, plan: KindPlan, actor_values: Mapping[str, str] + schema: Schema, + plan: KindPlan, + actor_values: Mapping[str, str], + all_demos: Sequence[Demo] = (), ) -> Optional[dict[str, Any]]: """A table that gained, in every update demonstration, a row naming the main record (a column holding its key) and the acting user (a column @@ -999,6 +1173,26 @@ def _audit_source( v = next(iter(values)) if v is not None and v.lower() in names and v.isidentifier(): where[col.name] = v + # Write events only: a column whose value in these rows (every + # update) differs from its value in the rows creates added to the + # same table (EspoCRM's ``action``: update, not create or read; + # Dolibarr's ``code``: COMPANY_MODIFY). + created = [ + ch.after or {} + for d in all_demos + if d not in plan.demos + for ch in d.diff.of(name, "added") + ] + for col in table.columns: + if col.name in (lc, ac) or col.name in table.primary_key: + continue + mine = {r.get(col.name) for r in rows} + theirs = {r.get(col.name) for r in created} + if len(mine) == 1 and created and len(theirs) == 1 and mine != theirs: + v = next(iter(mine)) + if v is not None and (col.kind == "text" or _WHOLE_NUMBER.match(v)): + if "\\" not in v and "'" not in v and "%" not in v: + where[col.name] = v found = { "kind": "audit", "table": name, @@ -1175,6 +1369,9 @@ def __init__(self, schema: Schema) -> None: self.params: dict[str, dict[str, str]] = {} #: Tables read through a view (the SQL guard refuses their name). self.views: dict[str, str] = {} + #: The main record's table: a key back to it names the record by its + #: own key, not by a display value, so no uniqueness check applies. + self.anchor = "" self._n = itertools.count() def q(self, name: str) -> str: @@ -1233,11 +1430,48 @@ def path(self, alias: str, table: str, loc: Location, depth: int = 0) -> str: raise GenerateError("a child hop can't be read as one value") sub = f"a{depth + 1}" inner = self.path(sub, hop.table, rest, depth + 1) + live = self.live(sub, hop.table) + if not rest.hops and hop.table != self.anchor: + # The value must name exactly one live row of its table: a + # second "Jon Park", or "Mary Ann|Lee" beside "Mary|Ann Lee", + # makes it NULL, so nothing matches and the write stops. + u = f"u{depth + 1}" + other = self.display(u, hop.table, rest.columns, rest.rule) + mine = self.display(sub, hop.table, rest.columns, rest.rule) + unique = ( + f"(SELECT COUNT(*) FROM {self.tref(hop.table)} {u} WHERE " + f"{self.same(other, mine, False)}{self.live_and(u, hop.table)}) = 1" + ) + inner = f"CASE WHEN {unique} THEN {inner} END" return ( f"(SELECT {inner} FROM {self.tref(hop.table)} {sub} WHERE " - f"{self.same(f'{sub}.{self.q(hop.ref_column)}', f'{alias}.{self.q(hop.column)}', hop.declared)})" + f"{self.same(f'{sub}.{self.q(hop.ref_column)}', f'{alias}.{self.q(hop.column)}', hop.declared)}" + f"{' AND ' + live if live else ''})" ) + def live(self, alias: str, table: str) -> str: + """The condition that a row isn't soft-deleted (a ``deleted`` or + ``is_deleted`` flag, or a ``deleted_at`` time), or "" when the table + has none: a trashed look-alike never resolves a key.""" + + t = self.schema.tables.get(table) + if t is None: + return "" + for name in ("deleted", "is_deleted"): + col = t.column(name) + if col is not None and col.kind in ("number", "bool"): + ref = f"{alias}.{self.q(name)}" + if self.d == "mysql" or col.kind == "number": + return f"COALESCE({ref}, 0) = 0" + return f"NOT COALESCE({ref}, false)" + if t.column("deleted_at") is not None: + return f"{alias}.{self.q('deleted_at')} IS NULL" + return "" + + def live_and(self, alias: str, table: str) -> str: + cond = self.live(alias, table) + return f" AND {cond}" if cond else "" + def same(self, a: str, b: str, declared: bool = True) -> str: if declared: return f"{a} = {b}" @@ -1400,13 +1634,17 @@ def build_contract( variant: Variant, flavor: str, settings: Settings, - bookkeeping: Mapping[str, set[str]], + excused: Mapping[str, set[str]], logs: set[str], ) -> dict[str, Any]: - """One ``openadapt.verify/v1`` document for one field set.""" + """One ``openadapt.verify/v1`` document for one field set. ``excused``: + table -> columns a person confirmed as the app's own bookkeeping; each + is excused only on the rows it was seen changing (the target, or a row + identified by key or by the task's values), never by name elsewhere.""" b = SqlBuilder(schema) b.views = dict(settings.views) + b.anchor = plan.anchor fields = set(variant.fields) anchor = plan.anchor surfaces: dict[str, Surface] = {} @@ -1436,10 +1674,21 @@ def build_contract( if plan.mode == "update" and p in plan.changes: target_fields.add(key) target_fields.update(loc.columns if not loc.hops else [loc.hops[0].column]) - if plan.mode == "create" and flavor == "create": + if plan.mode == "update" and flavor == "update": + # The target's own bookkeeping, excused on the target row only. + target_fields.update( + c for c in plan.anchor_extra if c in excused.get(anchor, set()) + ) + if plan.mode == "create": + # ``found`` pins the same constants as ``create`` (a soft-delete + # flag, a status, a type): a trashed record with the task's values + # is not "already there". Who created it may differ. roles = _anchor_roles(schema, plan, variant, settings.actor_params) for col, role in sorted(roles.items()): + if flavor == "found" and role.role in ("actor", "copied"): + continue _pin(b, main, anchor, col, role, match, settings.actor_params) + if plan.mode == "create" and flavor == "create": support = _support(plan, variant) for p in plan.optional: if p in fields or p in variant.lists: @@ -1488,6 +1737,14 @@ def build_contract( or (not found and plan.mode == "create" and set(g.co_owners) & fields) ] groups += [(p, g) for p, g in plan.lists.items() if p in variant.lists] + # A child row names its record by the record's identity: for an update + # only the values that pick the target, never the ones it changes (the + # target's older rows would otherwise move with the change). + main_params = ( + [ip for ip in identity_params if ip in plan.identity] + if plan.mode == "update" + else identity_params + ) for p, g in groups: name = f"rows:{g.table}" sname = _alias(name) @@ -1495,7 +1752,7 @@ def build_contract( scols = _base_columns(b, g.table) link = g.link # The main record's values, seen from the child row. - for ip in identity_params: + for ip in main_params: loc = locations[ip] up = Location( (Hop("fk", anchor, link.column, link.ref_column, True),) + loc.hops, @@ -1514,7 +1771,7 @@ def build_contract( items = range(variant.lists[p]) if g.list_param else [None] for i in items: m: dict[str, Any] = {} - for ip in identity_params: + for ip in main_params: loc = locations[ip] if loc.rule in ("number", "date", "datetime", "minutes"): m[f"main__{_alias(ip)}"] = {"literal": "1"} @@ -1536,7 +1793,7 @@ def build_contract( else: m[key] = _value_expr(pname, loc.rule) for col, role in sorted(g.roles.items()): - if role.role == "link" or found: + if role.role == "link" or (found and role.role in ("actor", "copied")): continue _pin(b, surf, g.table, col, role, m, settings.actor_params) effect = { @@ -1569,6 +1826,32 @@ def build_contract( ) if plan.mode == "update": target_fields.update(g.changed_columns) + # Existing rows the app changes beside the write (a counter row, the + # account a contact belongs to): declared by key or by the task's values, + # with only the confirmed columns allowed to move, on those rows only. + if not found: + for spec in plan.touched: + cols = [ + c for c in spec["columns"] if c in excused.get(spec["table"], set()) + ] + if not cols: + continue + name = f"raw_{_alias(spec['table'])}" + if name not in surfaces: + surfaces[name] = Surface( + name, + settings.views.get(spec["table"], spec["table"]), + _base_columns(b, spec["table"]), + ) + effects.append( + { + "kind": "record_written", + "probe": f"surface={name}|{spec['row']}, changed by the app", + "match": dict(spec["selector"]), + "expected_count": 1, + } + ) + target_fields.update(cols) # A new row a link row names (a new email address). for ref in plan.added_refs.values(): p = ref["param"] @@ -1619,21 +1902,8 @@ def build_contract( verifier["surfaces"][s.name] = sub # Main and child effects read named surfaces: no other read is judged # against them, and the whole-database frame covers every other table. - ignore = sorted( - {c for cols in bookkeeping.values() for c in cols} - & ( - _all_columns(schema, named_tables) - if schema.dialect == "mysql" - else _all_columns(schema, set(schema.tables)) - ) - ) frame: dict[str, Any] = {} - if ignore: - frame["ignore_fields"] = ignore - if plan.mode == "create" or flavor == "found": - frame["target_fields"] = [] - else: - frame["target_fields"] = sorted(target_fields) + frame["target_fields"] = [] if flavor == "found" else sorted(target_fields) if plan.mode == "update": # Another user's save to the target inside the window (a lost # update) shows only in the target's versions. @@ -1681,11 +1951,6 @@ def build_contract( } if nrf: db["new_row_fields"] = nrf - ign = { - t: sorted(c) for t, c in bookkeeping.items() if t not in named_tables and c - } - if ign: - db["ignore_columns"] = ign if settings.recheck_after_s: db["recheck_after_s"] = settings.recheck_after_s frame["database"] = db @@ -2186,6 +2451,23 @@ def check_read_only(connect: Any, dialect: str) -> list[str]: return problems +def read_account_notes(connect: Any, dialect: str) -> list[str]: + """What the read account can do beyond reading, that the contracts + still need (MySQL: TRIGGER, so the whole-database frame sees triggers; + it also lets the account create them).""" + + if dialect != "mysql": + return [] + return ( + [ + "the read account holds TRIGGER (needed to see triggers; it can also " + "create and drop triggers, so keep its password from the agent)" + ] + if _sees_triggers(connect) + else [] + ) + + def guard_problem(sql: str) -> Optional[str]: from openadapt_flow.runtime.effects.sql import assert_read_only_sql @@ -2211,6 +2493,17 @@ def _admin_connect(admin_read: Mapping[str, Any]) -> Any: return module.connect(**args) +def _account(conn: Any, reader: str) -> str: + """``'user'@'host'`` from ``user@host`` or ``'user'@'host'``, each part + escaped by the driver: the account name never reaches SQL as written.""" + + m = re.fullmatch(r"'?([^'@]+)'?(?:@'?([^']*)'?)?", reader.strip()) + if m is None: + raise GenerateError(f"--reader-account {reader!r} is not user@host") + user, host = m.group(1), m.group(2) if m.group(2) is not None else "%" + return f"{conn.escape(user)}@{conn.escape(host)}" + + def grant_trigger(admin_read: Mapping[str, Any], schema: Schema, reader: str) -> str: """MySQL shows a table's triggers only to an account with the TRIGGER privilege, and the whole-database frame must see them (a trigger planted @@ -2218,7 +2511,10 @@ def grant_trigger(admin_read: Mapping[str, Any], schema: Schema, reader: str) -> conn = _admin_connect(admin_read) try: - sql = f"GRANT TRIGGER ON {store.quote('mysql', schema.database)}.* TO {reader}" + sql = ( + f"GRANT TRIGGER ON {store.quote('mysql', schema.database)}.* TO " + f"{_account(conn, reader)}" + ) conn.cursor().execute(sql) conn.commit() finally: @@ -2299,7 +2595,7 @@ def create_views( f"CREATE OR REPLACE SQL SECURITY DEFINER VIEW {v} AS SELECT * FROM {q}" ) made[t] = f"{view_schema(schema)}.oa_read_{t}" - cur.execute(f"GRANT SELECT ON {vs}.* TO {reader}") + cur.execute(f"GRANT SELECT ON {vs}.* TO {_account(conn, reader)}") conn.commit() finally: conn.close() @@ -2400,79 +2696,154 @@ def propose( ) if not open_: answers = {**answers, f"locations.{p.kind}": chosen} + # Q: phone numbers. Comparing by digits is a normalization a person + # confirms per parameter; it is never inferred from a value's shape. + phones = sorted( + { + f"{p.kind}.{name}" + for p in plans + for name, loc in _all_locations(p) + if loc.rule == "phone" + } + ) + if phones: + confirmed = _answer(answers, "phones") + confirmed = set(confirmed) if isinstance(confirmed, list) else set() + questions.append( + Question( + "phones", + "These values were stored as other text with the same digits (a " + "phone number written with spaces stored without): " + + ", ".join(phones) + + ". Compare them as phone numbers (the same digits, at least 7, " + "and a task value that isn't shaped like a phone number is " + "compared exactly)? Answer `phones: [, ...]` with " + "the ones that are phone numbers; any other is compared exactly.", + phones, + ) + ) + for p in plans: + _confirm_phones( + p, {x.split(".", 1)[1] for x in confirmed if x.startswith(p.kind + ".")} + ) + # Q: bookkeeping, for the whole app at once. - logs = set().union(*(p.logs for p in plans)) if plans else set() - book: dict[str, set[str]] = {} + ans = _answer(answers, "bookkeeping") + accepted = ans == "accept" or isinstance(ans, dict) + ans = ans if isinstance(ans, dict) else {} + log_add = set(ans.get("logs_add", [])) + columns_add = {t: set(c) for t, c in dict(ans.get("columns_add", {})).items()} + columns_remove = {t: set(c) for t, c in dict(ans.get("columns_remove", {})).items()} + proposed: dict[str, set[str]] = {} + offered: dict[str, set[str]] = {} + where_seen: dict[str, set[str]] = {} + for p in plans: + for col, kind in p.anchor_extra.items(): + (proposed if kind in BOOKKEEPING_KINDS else offered).setdefault( + p.anchor, set() + ).add(col) + where_seen.setdefault(f"{p.anchor}", set()).add("the target row") + for row_spec in p.touched: + for col, kind in row_spec["columns"].items(): + (proposed if kind in BOOKKEEPING_KINDS else offered).setdefault( + row_spec["table"], set() + ).add(col) + where_seen.setdefault(row_spec["table"], set()).add(row_spec["row"]) + excused: dict[str, set[str]] = {} + if accepted: + for t in set(proposed) | set(columns_add): + excused[t] = (proposed.get(t, set()) | columns_add.get(t, set())) - ( + columns_remove.get(t, set()) + ) unchecked: dict[str, set[str]] = {} for p in plans: - for t, cols in p.bookkeeping.items(): - book.setdefault(t, set()).update(cols) if p.mode == "create" and p.demos: for v in [Variant(p.kind, sorted(p.locations))]: for col, role in _anchor_roles(schema, p, v, actor_params).items(): - if role.role == "varies": + if role.role in ("varies", "free", "timestamp"): unchecked.setdefault(p.anchor, set()).add(col) - log_add: set[str] = set() - log_remove: set[str] = set() - ans = _answer(answers, "bookkeeping") - if isinstance(ans, dict): - log_add, log_remove = ( - set(ans.get("logs_add", [])), - set(ans.get("logs_remove", [])), - ) - logs = (logs | log_add) - log_remove - for t, cols in dict(ans.get("columns_add", {})).items(): - book.setdefault(t, set()).update(cols) - for t, cols in dict(ans.get("columns_remove", {})).items(): - book[t] = book.get(t, set()) - set(cols) + for g in [*p.children.values(), *p.lists.values()]: + for col, role in g.roles.items(): + if role.role in ("varies", "free", "timestamp"): + unchecked.setdefault(g.table, set()).add(col) + candidates = set().union(*(p.log_candidates for p in plans)) if plans else set() + logs = log_add if accepted else set() sides = sorted({f"{s.table} ({s.count} per write)" for p in plans for s in p.sides}) for p in plans: # a table the person calls a log is no longer counted - p.sides = [s for s in p.sides if s.table not in log_add] - if logs or book or unchecked or sides: + p.sides = [x for x in p.sides if x.table not in logs] + p.logs = (candidates & logs) | (p.logs & logs) + unexcused = {t: c for p in plans for t, c in p.unexcused.items()} + if proposed or offered or unchecked or candidates or sides or unexcused: lines = [] + if proposed: + lines.append( + "Proposed as the app's own bookkeeping, excused only on the rows " + "the demonstrations changed (" + + "; ".join( + f"{t}: {', '.join(sorted(where_seen.get(t, set())))}" + for t in sorted(proposed) + ) + + "): " + + "; ".join( + f"{t}: {', '.join(sorted(c))}" for t, c in sorted(proposed.items()) + ) + + "." + ) + if offered: + lines.append( + "Also changed with no task value, and not excused unless you add " + "them under columns_add (a write that changes them stops): " + + "; ".join( + f"{t}: {', '.join(sorted(c))}" for t, c in sorted(offered.items()) + ) + + "." + ) + if unexcused: + lines.append( + "Changed on rows the generator can't identify, so a write that " + "changes them stops: " + + "; ".join( + f"{t}: {', '.join(sorted(c))}" for t, c in sorted(unexcused.items()) + ) + + "." + ) if sides: lines.append( "Rows the app adds beside each write, expected exactly that many " - "times (a second save adds a second one, which stops the write; " - "list a table under logs_add to allow any number): " - + ", ".join(sides) - + "." + "times: " + ", ".join(sides) + "." ) - if logs: + if candidates: lines.append( - "Tables treated as the app's own logs (rows may be added, existing " - "rows must not change" + "Tables that gained a different number of rows in different " + "demonstrations; they are not logs unless you list them under " + "logs_add (rows may then be added, existing rows must not change" + ( ")" if dialect == "mysql" - else "; on PostgreSQL they are not read at all)" + else "; on PostgreSQL a log table is not read at all)" ) + ": " - + ", ".join(sorted(logs)) - ) - if book: - lines.append( - "Columns that change on their own and are not compared: " - + "; ".join( - f"{t}: {', '.join(sorted(c))}" for t, c in sorted(book.items()) if c - ) + + ", ".join(sorted(candidates)) + + "." ) if unchecked: lines.append( - "Columns of a new record that differ between demonstrations and are " - "not checked: " + "Columns of a new record that are not checked (they differ between " + "demonstrations, are free text every demonstration left empty, or " + "are the write's time stamp): " + "; ".join( f"{t}: {', '.join(sorted(c))}" for t, c in sorted(unchecked.items()) ) + + "." ) questions.append( Question( "bookkeeping", - "Is this the app's own bookkeeping, which a write may change without " - "a task asking? " + "Is this the app's own bookkeeping? " + " ".join(lines) - + " Answer `bookkeeping: accept`, or list changes under " - "logs_add, logs_remove, columns_add, columns_remove.", + + " Answer `bookkeeping: accept`, or a mapping with any of " + "logs_add, columns_add, columns_remove. Unanswered, nothing is " + "excused and no table is a log.", "accept", ) ) @@ -2552,6 +2923,7 @@ def propose( } report: dict[str, Any] = { "applied": applied, + "read_account": read_account_notes(connect, dialect), "dialect": dialect, "tables": len(schema.tables), "questions": [ @@ -2597,6 +2969,9 @@ def propose( for s in p.sides ], "logs": sorted(p.logs), + "log_candidates": sorted(p.log_candidates), + "touched_rows": p.touched, + "unexcused": {t: sorted(c) for t, c in p.unexcused.items()}, "refused": list(p.refused), "unresolved": { k: [loc.describe(p.anchor) for loc in v] @@ -2626,8 +3001,9 @@ def propose( continue ok = True for flavor in flavors: - kind_logs = (p.logs | log_add) - log_remove - doc = build_contract(schema, p, v, flavor, settings, book, kind_logs) + doc = build_contract( + schema, p, v, flavor, settings, excused, set(p.logs) + ) rel = Path("contracts") / p.kind / f"{v.slug}.{flavor}.yaml" path = out / rel path.parent.mkdir(parents=True, exist_ok=True) @@ -2742,8 +3118,6 @@ def choose( contract fits: the write is then stopped, never sent unchecked. """ - from openadapt_flow.verifier import Contract - manifest_path = Path(manifest_path) manifest = load_yaml(manifest_path) or {} if manifest.get("schema_version") != MANIFEST_SCHEMA: @@ -2769,10 +3143,10 @@ def choose( base = manifest_path.parent # The record already holds the task's values (found, or an update # already in place): nothing may change. Else the create or update. - found = Contract.load(base / var["found"]) + found = _load_for(base / var["found"], params) if _exists(found, params): return found, params - return Contract.load(base / var[entry["mode"]]), params + return _load_for(base / var[entry["mode"]], params), params raise GenerateError( f"no {kind} contract for the fields {sorted(given)}" + (f" and lists {lists}" if lists else "") @@ -2780,6 +3154,32 @@ def choose( ) +def _load_for(path: Path, params: Mapping[str, Any]) -> Any: + """The contract at ``path`` for these values. A value the contract + compares as a phone number that isn't written like one (fewer than 7 + digits, or other characters) is compared exactly instead.""" + + from openadapt_flow.generate.demo_analysis import phone_shaped + from openadapt_flow.verifier import Contract + + doc = load_yaml(path) or {} + changed = False + for effect in doc.get("effects") or []: + for value in (effect.get("match") or {}).values(): + if ( + isinstance(value, dict) + and value.get("kind") == "phone" + and not phone_shaped(params.get(str(value.get("param")))) + ): + value.pop("kind") + changed = True + if not changed: + return Contract.load(path) + contract = Contract.from_dict(doc, name=Path(path).stem) + contract.config._base_dir = Path(path).resolve().parent + return contract + + def _exists(contract: Any, params: Mapping[str, Any]) -> bool: """Does every effect of the found contract hold now (the record, and each row beside it that holds a value)?""" diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index 62247e50d9..ddda26344c 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -294,7 +294,10 @@ def demo(kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: assert gen.main(argv) == 0 report = json.loads((work / "out" / "report.json").read_text()) # The person accepts the proposals. + # The person accepts the proposals, and says journal_details (one row + # per changed field) is the app's log. answers = {q["key"]: q["proposal"] for q in report["questions"]} + answers["bookkeeping"] = {"logs_add": ["journal_details"]} (work / "answers.yaml").write_text(yaml.safe_dump(answers)) assert gen.main([*argv, "--answers", str(work / "answers.yaml")]) == 0 if os.environ.get("OA_GEN_TEST_KEEP"): @@ -344,7 +347,7 @@ def test_questions_and_contracts(generated: dict[str, Any]) -> None: assert kinds["update_issue"]["emitted"] == 31, json.dumps( kinds["update_issue"], indent=1 ) - assert "journal_details" in kinds["update_issue"]["logs"] + assert "journal_details" in kinds["update_issue"]["log_candidates"] def test_a_correct_create_is_confirmed(generated: dict[str, Any]) -> None: diff --git a/packages/flow/tests/test_generate_verify_contract_attacks.py b/packages/flow/tests/test_generate_verify_contract_attacks.py new file mode 100644 index 0000000000..141d0eebdd --- /dev/null +++ b/packages/flow/tests/test_generate_verify_contract_attacks.py @@ -0,0 +1,263 @@ +# ruff: noqa: F811 (pytest fixtures imported from the generator test, then used as arguments) +"""The adversarial review's attacks on generated contracts (#1672 round 1). + +Each test is one way a wrong write could have passed, from the review, run +on the same throwaway PostgreSQL as ``test_generate_verify_contract``. Every +wrong write must now stop, and a correct update of an issue that already has +history must be CONFIRMED. Records are made up. +""" + +from __future__ import annotations + +import json +import os +from typing import Any + +import pytest +import yaml + +psycopg2 = pytest.importorskip("psycopg2") + +from openadapt_flow.generate import verify_contract as gen # noqa: E402 +from tests.test_generate_verify_contract import ( # noqa: E402,F401 + BOT, + CREATE, + KINDS, + SCHEMA, + App, + _run, + generated, + pg, +) + + +def _fresh_issue(app: App, subject: str) -> int: + return app.create_issue( + {"project": "Help Desk", "subject": subject, "status": "New", + "assignee": None, "due_date": None, "customer_ref": None} + ) # fmt: skip + + +def test_a1_a_number_written_with_the_same_digits_is_stopped( + generated: dict[str, Any], +) -> None: + """The custom field is stored as given, so it is compared exactly; the + phone rule applies only to a value a person confirmed as a phone.""" + + app = generated["app"] + for asked, written in (("1500.75", "15007.5"), ("2026-1104", "202-611-04")): + v = {**CREATE, "subject": f"A1 {asked}", "customer_ref": asked} + out = _run(generated, "create_issue", v, + lambda v=v, w=written: app.create_issue({**v, "customer_ref": w})) # fmt: skip + assert not out.startswith("CONFIRMED"), out + + +def test_a2_a_display_name_two_users_share_is_stopped( + generated: dict[str, Any], +) -> None: + app = generated["app"] + other = app.one( + "INSERT INTO users (login, firstname, lastname) VALUES ('jpark2','Jon','Park') RETURNING id" + ) + try: + v = {**CREATE, "subject": "A2 shared name", "assignee": "Jon Park"} + + def write() -> None: + iid = app.create_issue({**v, "assignee": None}) + app.q("UPDATE issues SET assigned_to_id=%s WHERE id=%s", (other, iid)) + + out = _run(generated, "create_issue", v, write) + assert not out.startswith("CONFIRMED"), out + finally: + app.q("UPDATE issues SET assigned_to_id=NULL WHERE assigned_to_id=%s", (other,)) + app.q("DELETE FROM users WHERE id=%s", (other,)) + + +def test_a3_a_first_and_last_name_split_two_ways_is_stopped( + generated: dict[str, Any], +) -> None: + app = generated["app"] + a = app.one( + "INSERT INTO users (login, firstname, lastname) VALUES ('mal','Mary Ann','Lee') RETURNING id" + ) + b = app.one( + "INSERT INTO users (login, firstname, lastname) VALUES ('mal2','Mary','Ann Lee') RETURNING id" + ) + try: + v = {**CREATE, "subject": "A3 split", "assignee": "Mary Ann Lee"} + + def write() -> None: + iid = app.create_issue({**v, "assignee": None}) + app.q("UPDATE issues SET assigned_to_id=%s WHERE id=%s", (b, iid)) + + out = _run(generated, "create_issue", v, write) + assert not out.startswith("CONFIRMED"), out + finally: + app.q( + "UPDATE issues SET assigned_to_id=NULL WHERE assigned_to_id IN (%s,%s)", + (a, b), + ) + app.q("DELETE FROM users WHERE id IN (%s,%s)", (a, b)) + + +@pytest.mark.xfail( + strict=True, + reason="PostgreSQL has no whole-database frame: a table a person names as a " + "log is not read, so a rewrite of its existing rows is not seen (documented " + "in GENERATE_CONTRACTS.md, Limits)", +) +def test_a4_a_rewrite_of_a_log_table_on_postgresql_is_stopped( + generated: dict[str, Any], +) -> None: + app = generated["app"] + iid = _fresh_issue(app, "A4 fresh") + v = {"project": "Help Desk", "subject": "A4 fresh", "status": "Resolved", "assignee": None, + "due_date": None, "done_ratio": None, "notes": None} # fmt: skip + app.q( + "INSERT INTO journal_details (journal_id, prop_key, old_value, value) VALUES (999,'status_id','1','2')" + ) + + def write() -> None: + app.update_issue(iid, v) + app.q("UPDATE journal_details SET value='999' WHERE journal_id=999") + + out = _run(generated, "update_issue", v, write) + assert not out.startswith("CONFIRMED"), out + + +def test_a5_a_correct_update_of_an_issue_with_history_is_confirmed( + generated: dict[str, Any], +) -> None: + app = generated["app"] + iid = _fresh_issue(app, "A5 history") + first = {"project": "Help Desk", "subject": "A5 history", "status": "In Progress", "assignee": None, + "due_date": None, "done_ratio": None, "notes": None} # fmt: skip + app.update_issue( + iid, first, user=2 + ) # earlier history, by a colleague, before the window + v = {**first, "status": "Resolved"} + out = _run(generated, "update_issue", v, lambda: app.update_issue(iid, v)) + assert out.startswith("CONFIRMED"), out + + +# -- an app that touches a counter row on each create, and soft-deletes ------------- + +SCHEMA_B = ( + SCHEMA.replace( + "lock_version integer NOT NULL DEFAULT 0);", + "lock_version integer NOT NULL DEFAULT 0, deleted smallint NOT NULL DEFAULT 0);", + ) + .replace( + "INSERT INTO settings (name, value) VALUES ('ui_theme', 'classic');", + "INSERT INTO settings (name, value) VALUES ('ui_theme', 'classic'), ('last_issue', '0');", + ) + .replace("CREATE ROLE oa_reader LOGIN PASSWORD 'reader-pw';", "") +) + + +class AppB(App): + def create_issue(self, v: dict[str, Any], *, project_id: Any = None) -> int: + iid = super().create_issue(v, project_id=project_id) + self.q("UPDATE settings SET value=%s WHERE name='last_issue'", (str(iid),)) + return iid + + +@pytest.fixture(scope="module") +def gen_b(pg: Any) -> dict[str, Any]: + root, port = pg["root"], pg["port"] + admin = psycopg2.connect( + host="127.0.0.1", port=port, user="admin", dbname="postgres" + ) + admin.autocommit = True + admin.cursor().execute("CREATE DATABASE trackerb") + admin.close() + app = AppB(port, "trackerb") + with app.con() as c, c.cursor() as cur: + cur.execute( + "DO $$BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='oa_reader') " + "THEN CREATE ROLE oa_reader LOGIN PASSWORD 'reader-pw'; END IF; END$$" + ) + cur.execute(SCHEMA_B) + os.environ["OA_GEN_TEST_PW"] = "reader-pw" + work = root / "workb" + work.mkdir() + read = {"sql_driver": "psycopg2", + "sql_connect_args": {"host": "127.0.0.1", "port": port, "user": "oa_reader", "dbname": "trackerb"}, + "sql_password_env": "OA_GEN_TEST_PW"} # fmt: skip + (work / "read.yaml").write_text(yaml.safe_dump(read)) + kinds = {"kinds": [KINDS["kinds"][0]]} + (work / "kinds.yaml").write_text(yaml.safe_dump(kinds, allow_unicode=True)) + common = ["--read", str(work / "read.yaml")] + for ex in kinds["kinds"][0]["examples"]: + assert gen.main(["snapshot", *common, "--out", str(work / "before.json")]) == 0 + app.create_issue(ex["values"]) + assert gen.main(["record", *common, "--before", str(work / "before.json"), "--kind", "create_issue", + "--values", json.dumps(ex["values"]), "--demos", str(work / "demos")]) == 0 # fmt: skip + argv = ["propose", *common, "--kinds", str(work / "kinds.yaml"), "--demos", str(work / "demos"), + "--out", str(work / "out"), "--actor-param", f"bot_user_id={BOT}"] # fmt: skip + assert gen.main(argv) == 0 + report = json.loads((work / "out" / "report.json").read_text()) + answers = {q["key"]: q["proposal"] for q in report["questions"]} + (work / "answers.yaml").write_text(yaml.safe_dump(answers)) + assert gen.main([*argv, "--answers", str(work / "answers.yaml")]) == 0 + report = json.loads((work / "out" / "report.json").read_text()) + return {"app": app, "out": work / "out", "report": report, "read": read} + + +def test_b0_the_counter_row_is_excused_by_key_and_a_correct_create_passes( + gen_b: dict[str, Any], +) -> None: + rows = gen_b["report"]["kinds"]["create_issue"]["touched_rows"] + assert [(r["table"], r["columns"]) for r in rows] == [ + ("settings", {"value": "counter"}) + ] + app = gen_b["app"] + v = {**CREATE, "subject": "B0 correct"} + out = _run(gen_b, "create_issue", v, lambda: app.create_issue(v)) + assert out.startswith("CONFIRMED"), out + + +def test_b1_another_setting_changed_is_stopped(gen_b: dict[str, Any]) -> None: + app = gen_b["app"] + v = {**CREATE, "subject": "B1"} + try: + out = _run(gen_b, "create_issue", v, lambda: ( + app.create_issue(v), app.q("UPDATE settings SET value='dark' WHERE name='ui_theme'"))) # fmt: skip + finally: + app.q("UPDATE settings SET value='classic' WHERE name='ui_theme'") + assert not out.startswith("CONFIRMED"), out + + +def test_b2_another_records_custom_field_changed_is_stopped( + gen_b: dict[str, Any], +) -> None: + app = gen_b["app"] + victim = app.create_issue({**CREATE, "subject": "victim", "customer_ref": "777"}) + v = {**CREATE, "subject": "B2"} + out = _run(gen_b, "create_issue", v, lambda: ( + app.create_issue(v), + app.q("UPDATE custom_values SET value='HIJACKED' WHERE customized_id=%s", (victim,)))) # fmt: skip + assert not out.startswith("CONFIRMED"), out + + +def test_b3_a_trashed_record_with_the_values_is_not_found( + gen_b: dict[str, Any], +) -> None: + app = gen_b["app"] + v = {**CREATE, "subject": "B3 trashed"} + iid = app.create_issue(v) + app.q("UPDATE issues SET deleted=1 WHERE id=%s", (iid,)) + out = _run(gen_b, "create_issue", v, lambda: None) + assert not out.startswith("CONFIRMED"), out + + +def test_b4_a_create_that_writes_deleted_is_stopped(gen_b: dict[str, Any]) -> None: + app = gen_b["app"] + v = {**CREATE, "subject": "B4"} + + def write() -> None: + iid = app.create_issue(v) + app.q("UPDATE issues SET deleted=1 WHERE id=%s", (iid,)) + + out = _run(gen_b, "create_issue", v, write) + assert not out.startswith("CONFIRMED"), out From 6f5b0f6486e4da8877e4eca03069b6ffed9eafd5 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 19:27:24 -0400 Subject: [PATCH 76/94] A row the app changes by key is excused for every kind, on that row only --- .../generate/verify_contract.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 70e9636752..8ee3036466 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1073,7 +1073,13 @@ def _touched_rows( for c in cols } plan.touched.append( - {"table": table, "selector": selector, "columns": kinds, "row": how} + { + "table": table, + "selector": selector, + "columns": kinds, + "row": how, + "by_key": " row " in how, + } ) @@ -2727,6 +2733,24 @@ def propose( p, {x.split(".", 1)[1] for x in confirmed if x.startswith(p.kind + ".")} ) + # A row the app changes by key (a user's preferences, a counter) is the + # app's, whichever kind of write a demonstration happened to make when + # it changed: the same excuse, on that row only, applies to every kind. + keyed: dict[tuple[str, str], dict[str, Any]] = {} + for p in plans: + for row_spec in p.touched: + if row_spec.get("by_key"): + k = ( + row_spec["table"], + json.dumps(row_spec["selector"], sort_keys=True), + ) + merged = keyed.setdefault(k, {**row_spec, "columns": {}}) + merged["columns"].update(row_spec["columns"]) + for p in plans: + p.touched = [x for x in p.touched if not x.get("by_key")] + [ + {**x, "columns": dict(x["columns"])} for x in keyed.values() + ] + # Q: bookkeeping, for the whole app at once. ans = _answer(answers, "bookkeeping") accepted = ans == "accept" or isinstance(ans, dict) From 727296567a04dbb9636f5e3ea08d36b0ac03c3d0 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 19:43:24 -0400 Subject: [PATCH 77/94] found: a row the app changes by key is excused there too --- .../generate/verify_contract.py | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 8ee3036466..880f56c33a 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1835,29 +1835,32 @@ def build_contract( # Existing rows the app changes beside the write (a counter row, the # account a contact belongs to): declared by key or by the task's values, # with only the confirmed columns allowed to move, on those rows only. - if not found: - for spec in plan.touched: - cols = [ - c for c in spec["columns"] if c in excused.get(spec["table"], set()) - ] - if not cols: - continue - name = f"raw_{_alias(spec['table'])}" - if name not in surfaces: - surfaces[name] = Surface( - name, - settings.views.get(spec["table"], spec["table"]), - _base_columns(b, spec["table"]), - ) - effects.append( - { - "kind": "record_written", - "probe": f"surface={name}|{spec['row']}, changed by the app", - "match": dict(spec["selector"]), - "expected_count": 1, - } + touched_fields: set[str] = set() + for spec in plan.touched: + # A row changed by key (a user's preferences, which even a read may + # touch) is excused in ``found`` too; a row named by the task's + # values only when something is written. + if found and not spec.get("by_key"): + continue + cols = [c for c in spec["columns"] if c in excused.get(spec["table"], set())] + if not cols: + continue + name = f"raw_{_alias(spec['table'])}" + if name not in surfaces: + surfaces[name] = Surface( + name, + settings.views.get(spec["table"], spec["table"]), + _base_columns(b, spec["table"]), ) - target_fields.update(cols) + effects.append( + { + "kind": "record_written", + "probe": f"surface={name}|{spec['row']}, changed by the app", + "match": dict(spec["selector"]), + "expected_count": 1, + } + ) + touched_fields.update(cols) # A new row a link row names (a new email address). for ref in plan.added_refs.values(): p = ref["param"] @@ -1909,7 +1912,9 @@ def build_contract( # Main and child effects read named surfaces: no other read is judged # against them, and the whole-database frame covers every other table. frame: dict[str, Any] = {} - frame["target_fields"] = [] if flavor == "found" else sorted(target_fields) + frame["target_fields"] = sorted( + touched_fields | (set() if flavor == "found" else target_fields) + ) if plan.mode == "update": # Another user's save to the target inside the window (a lost # update) shows only in the target's versions. From d18b123b9f79f19745a29c64e8ce4431395e3cd6 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 20:11:49 -0400 Subject: [PATCH 78/94] Uniqueness over every value the task reads through one key together --- .../generate/verify_contract.py | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 880f56c33a..9c0b6b6c3c 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1375,6 +1375,9 @@ def __init__(self, schema: Schema) -> None: self.params: dict[str, dict[str, str]] = {} #: Tables read through a view (the SQL guard refuses their name). self.views: dict[str, str] = {} + #: (table, key column) -> the displays the task's values are read + #: through that key with: checked together for uniqueness. + self.companions: dict[tuple[str, str], list[tuple[tuple[str, ...], str]]] = {} #: The main record's table: a key back to it names the record by its #: own key, not by a display value, so no uniqueness check applies. self.anchor = "" @@ -1442,11 +1445,23 @@ def path(self, alias: str, table: str, loc: Location, depth: int = 0) -> str: # second "Jon Park", or "Mary Ann|Lee" beside "Mary|Ann Lee", # makes it NULL, so nothing matches and the write stops. u = f"u{depth + 1}" - other = self.display(u, hop.table, rest.columns, rest.rule) - mine = self.display(sub, hop.table, rest.columns, rest.rule) + # Every value the task gives through this key together (a + # customer's name and town) must name one row: a name shared by + # two towns is fine when the town tells them apart. + displays = self.companions.get((hop.table, hop.column)) or [ + (rest.columns, rest.rule) + ] + conds = [ + self.same( + self.display(u, hop.table, cols, rule), + self.display(sub, hop.table, cols, rule), + False, + ) + for cols, rule in displays + ] unique = ( f"(SELECT COUNT(*) FROM {self.tref(hop.table)} {u} WHERE " - f"{self.same(other, mine, False)}{self.live_and(u, hop.table)}) = 1" + f"{' AND '.join(conds)}{self.live_and(u, hop.table)}) = 1" ) inner = f"CASE WHEN {unique} THEN {inner} END" return ( @@ -1651,6 +1666,17 @@ def build_contract( b = SqlBuilder(schema) b.views = dict(settings.views) b.anchor = plan.anchor + wanted = set(variant.fields) | set(variant.lists) + list_keys = {k for g in plan.lists.values() for k in g.params} + for c_name, c_loc in _all_locations(plan): + if c_name not in wanted and c_name not in list_keys: + continue + if not c_loc.hops or c_loc.hops[-1].kind != "fk": + continue + c_key = (c_loc.hops[-1].table, c_loc.hops[-1].column) + c_entry = (tuple(c_loc.columns), c_loc.rule) + if c_entry not in b.companions.setdefault(c_key, []): + b.companions[c_key].append(c_entry) fields = set(variant.fields) anchor = plan.anchor surfaces: dict[str, Surface] = {} From b11cfed6908fd243f596282fc875892f85897cb1 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 20:37:22 -0400 Subject: [PATCH 79/94] PostgreSQL: the whole-database frame and the recheck question, as on MySQL --- CAPABILITIES.md | 2 +- packages/flow/docs/GENERATE_CONTRACTS.md | 22 +- .../generate/verify_contract.py | 250 ++++++++---------- .../tests/test_generate_verify_contract.py | 1 + .../test_generate_verify_contract_attacks.py | 6 - 5 files changed, 117 insertions(+), 164 deletions(-) diff --git a/CAPABILITIES.md b/CAPABILITIES.md index e87ee02613..1a59e9f3ce 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,7 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index db9d014833..7730d44f73 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -216,14 +216,14 @@ columns set to the acting user are proposed; anything else is listed and excused only if the person adds it (`columns_add`). A changed row the generator can't identify is listed too, and a write that changes it stops. -**The frame.** On MySQL and MariaDB every contract uses the whole-database -frame (`frame.database`): a row added, removed or changed in any table that -the contract doesn't declare stops the write, except rows added to a table -the person named as a log. The main record's table and the tables of rows -that hold values are read by the contract's own reads, with every column. -On PostgreSQL, `frame.database` isn't available, so every other table is read -as its own surface, except the tables the person named as logs, which are -not read at all. +**The frame.** Every contract uses the whole-database frame +(`frame.database`, with the store's dialect): a row added, removed or changed +in any table that the contract doesn't declare stops the write, except rows +added to a table the person named as a log. The main record's table and the +tables of rows that hold values are read by the contract's own reads, with +every column. On PostgreSQL the read account also needs `SELECT` on the +sequences (see [VERIFY_RUN.md](VERIFY_RUN.md)); without it the dry read +fails and no contract is written. **Lost updates.** An update's contracts set `frame.versions`, so another user's save to the target inside the window stops the write. The generator @@ -240,7 +240,7 @@ version check, and `report.json` shows `versions: null`. If the app stops writing the trail (an agenda setting turned off), the agent's own save has no new version, and the verifier stops the write rather than pass it. -**The recheck window.** On MySQL and MariaDB, one question asks how long the +**The recheck window.** One question asks how long the app can still write on its own after a verified write. The answer is `frame.database.recheck_after_s`; the proposal is 300 seconds. @@ -285,10 +285,6 @@ why. - With one demonstration of a create, every other column must hold the value it held then, including one computed from the task's values (a price with tax). `QUESTIONS.md` says so; record a second create with other values. -- On PostgreSQL there is no whole-database frame, so there is no recheck (a - write that lands after the check is not seen), and a table the person names - as a log is not read at all: a rewrite of its existing rows passes. Every - other table is read and compared. - REST metadata (OpenAPI) is not read yet. Apps without database access need hand-written contracts. - The frame doesn't attribute changes to other users: another user's edit to diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 9c0b6b6c3c..bb0b060b04 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1947,67 +1947,48 @@ def build_contract( versions = _versions_frame(b, plan, settings) if versions is not None: frame["versions"] = versions - if schema.dialect == "mysql": - allow: list[dict[str, Any]] = [] - for t in sorted(logs): - if t in named_tables: - continue + # MySQL, MariaDB and PostgreSQL: the whole-database frame + allow: list[dict[str, Any]] = [] + for t in sorted(logs): + if t in named_tables: + continue + allow.append({"table": t}) + for t in sorted(named_tables): + if plan.mode == "create" or flavor == "found": + # The contract's own read judges the rows the write adds; the + # whole-database frame still sees any existing row change, + # now and at the recheck (a late edit to a look-alike). allow.append({"table": t}) - for t in sorted(named_tables): - if plan.mode == "create" or flavor == "found": - # The contract's own read judges the rows the write adds; the - # whole-database frame still sees any existing row change, - # now and at the recheck (a late edit to a look-alike). - allow.append({"table": t}) - continue - # An update changes a row here, which an append-only table - # can't allow: its own read judges this table, and the recheck - # (database only) doesn't see it. - allow.append( - { - "table": t, - "mode": "skip", - "reason": "read in full, every column, by this contract's own surface", - } - ) - views = { - s.table: s.name for s in surfaces.values() if s.name.startswith("raw_") - } - db: dict[str, Any] = { - "dialect": "mysql", - "read": dict(settings.read), - "allow_tables": allow, - "views": views, - "settle_s": 1, - "settle_attempts": 5, - } - nrf = { - s.table: sorted(getattr(s, "_new_row_fields", [])) - for s in surfaces.values() - if s.name.startswith("raw_") and getattr(s, "_new_row_fields", None) - } - if nrf: - db["new_row_fields"] = nrf - if settings.recheck_after_s: - db["recheck_after_s"] = settings.recheck_after_s - frame["database"] = db - else: - # PostgreSQL: frame.database reads MySQL and SQLite only, so every - # other table is read as its own surface (the read-set frame). - for t in sorted(schema.tables): - if t in named_tables or t in logs: - continue - if f"raw_{_alias(t)}" in surfaces: - continue - s = Surface( - f"raw_{_alias(t)}", settings.views.get(t, t), _base_columns(b, t) - ) - surfaces[s.name] = s - verifier["surfaces"][s.name] = { - "kind": "sql", - **settings.read, - "sql_query": s.sql(b), + continue + # An update changes a row here, which an append-only table + # can't allow: its own read judges this table, and the recheck + # (database only) doesn't see it. + allow.append( + { + "table": t, + "mode": "skip", + "reason": "read in full, every column, by this contract's own surface", } + ) + views = {s.table: s.name for s in surfaces.values() if s.name.startswith("raw_")} + db: dict[str, Any] = { + "dialect": schema.dialect, + "read": dict(settings.read), + "allow_tables": allow, + "views": views, + "settle_s": 1, + "settle_attempts": 5, + } + nrf = { + s.table: sorted(getattr(s, "_new_row_fields", [])) + for s in surfaces.values() + if s.name.startswith("raw_") and getattr(s, "_new_row_fields", None) + } + if nrf: + db["new_row_fields"] = nrf + if settings.recheck_after_s: + db["recheck_after_s"] = settings.recheck_after_s + frame["database"] = db doc: dict[str, Any] = { "schema_version": "openadapt.verify/v1", "environment_id": settings.environment_id, @@ -2098,10 +2079,9 @@ def _raw_effect( ) -> dict[str, Any]: """An effect on a table's own read: rows the app adds beside the write. - On MySQL the table is judged by the whole-database frame, which also - checks that every column these rows set beyond ``match`` is declared in - ``new_row_fields`` or holds its default. On PostgreSQL the surface reads - every column as text, so constants are pinned in ``match`` itself.""" + The table is judged by the whole-database frame, which also checks that + every column these rows set beyond ``match`` is declared in + ``new_row_fields`` or holds its default.""" name = f"raw_{_alias(table)}" surf = surfaces.get(name) @@ -2110,69 +2090,58 @@ def _raw_effect( surfaces[name] = surf t = schema.tables[table] m = dict(match) - if schema.dialect == "mysql": - # Text and whole-number constants only: the snapshot's JSON form of - # a decimal or a date isn't the generator's text form. - for col, value in list(m.items()): - c = t.column(col) - lit = value.get("literal") if isinstance(value, dict) else None - if ( - c is not None - and lit is not None - and not ( - c.kind == "text" - or ( - c.kind in ("number", "bool") - and c.data_type.startswith( - ( - "int", - "tinyint", - "smallint", - "mediumint", - "bigint", - "integer", - ) + # Text and whole-number constants only: the snapshot's JSON form of + # a decimal or a date isn't the generator's text form. + for col, value in list(m.items()): + c = t.column(col) + lit = value.get("literal") if isinstance(value, dict) else None + if ( + c is not None + and lit is not None + and not ( + c.kind == "text" + or ( + c.kind in ("number", "bool") + and c.data_type.startswith( + ( + "int", + "tinyint", + "smallint", + "mediumint", + "bigint", + "integer", ) - and re.fullmatch(r"-?\d+", lit) ) + and re.fullmatch(r"-?\d+", lit) ) + ) + ): + m.pop(col) + nrf = set(getattr(surf, "_new_row_fields", set())) + for c in t.columns: + if c.name in m: + continue + role = roles.get(c.name) + if role is not None and role.role == "constant" and role.value is None: + continue + if ( + role is None + or role.role != "constant" + or (role.detail or {}).get("is_default") + ): + if role is None or role.role in ( + "key", + "timestamp", + "link", + "varies", + "copied", + "actor", + "free", ): - m.pop(col) - nrf = set(getattr(surf, "_new_row_fields", set())) - for c in t.columns: - if c.name in m: - continue - role = roles.get(c.name) - if role is not None and role.role == "constant" and role.value is None: - continue - if ( - role is None - or role.role != "constant" - or (role.detail or {}).get("is_default") - ): - if role is None or role.role in ( - "key", - "timestamp", - "link", - "varies", - "copied", - "actor", - "free", - ): - nrf.add(c.name) - continue - nrf.add(c.name) - surf.__dict__["_new_row_fields"] = nrf - else: - for col, role in sorted(roles.items()): - if role.role == "constant" and col not in m: - if role.value is None: - alias = f"pin__{_alias(col)}" - if alias not in {a for a, _ in surf.columns}: - surf.columns.append((alias, b.pin("t", table, col, None))) - m[alias] = {"literal": "1"} - else: - m[col] = {"literal": role.value} + nrf.add(c.name) + continue + nrf.add(c.name) + surf.__dict__["_new_row_fields"] = nrf return { "kind": "record_written", "probe": f"surface={name}|{note}", @@ -2872,11 +2841,7 @@ def propose( "Tables that gained a different number of rows in different " "demonstrations; they are not logs unless you list them under " "logs_add (rows may then be added, existing rows must not change" - + ( - ")" - if dialect == "mysql" - else "; on PostgreSQL a log table is not read at all)" - ) + + ")" + ": " + ", ".join(sorted(candidates)) + "." @@ -2902,21 +2867,20 @@ def propose( "accept", ) ) - # Q: the recheck window (MySQL: frame.database.recheck_after_s). + # Q: the recheck window (frame.database.recheck_after_s). recheck: Optional[float] = None - if dialect == "mysql": - a = _answer(answers, "recheck_after_s") - recheck = float(a) if isinstance(a, (int, float)) and a > 0 else None - questions.append( - Question( - "recheck_after_s", - "How long after a verified write can the app still write on its own " - "(a background job, a delayed retry)? The database is read again that " - f"many seconds later and a late change refutes the write. Proposed: " - f"{DEFAULT_RECHECK_S}. Answer `recheck_after_s: `.", - DEFAULT_RECHECK_S, - ) + a = _answer(answers, "recheck_after_s") + recheck = float(a) if isinstance(a, (int, float)) and a > 0 else None + questions.append( + Question( + "recheck_after_s", + "How long after a verified write can the app still write on its own " + "(a background job, a delayed retry)? The database is read again that " + f"many seconds later and a late change refutes the write. Proposed: " + f"{DEFAULT_RECHECK_S}. Answer `recheck_after_s: `.", + DEFAULT_RECHECK_S, ) + ) for p in plans: # child groups of values placed after analysis _children(schema, p, [], only_missing=True) questions.extend(location_questions) @@ -2924,7 +2888,7 @@ def propose( settings = Settings( read, dict(actor_params), - recheck or (DEFAULT_RECHECK_S if dialect == "mysql" else None), + recheck or DEFAULT_RECHECK_S, env, ) @@ -3078,9 +3042,7 @@ def propose( for s in doc["verifier"]["surfaces"].values(): why = why or guard_problem(s["sql_query"]) if why is None and validate: - why = dry_read( - path, flatten(sample_values(p, v, demos)), dialect == "mysql" - ) + why = dry_read(path, flatten(sample_values(p, v, demos)), True) if why is not None: path.unlink() rep["rejected"].append( diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index ddda26344c..9bd166e70e 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -62,6 +62,7 @@ INSERT INTO settings (name, value) VALUES ('ui_theme', 'classic'); CREATE ROLE oa_reader LOGIN PASSWORD 'reader-pw'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO oa_reader; +GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO oa_reader; """ BOT = 1 diff --git a/packages/flow/tests/test_generate_verify_contract_attacks.py b/packages/flow/tests/test_generate_verify_contract_attacks.py index 141d0eebdd..9188445071 100644 --- a/packages/flow/tests/test_generate_verify_contract_attacks.py +++ b/packages/flow/tests/test_generate_verify_contract_attacks.py @@ -100,12 +100,6 @@ def write() -> None: app.q("DELETE FROM users WHERE id IN (%s,%s)", (a, b)) -@pytest.mark.xfail( - strict=True, - reason="PostgreSQL has no whole-database frame: a table a person names as a " - "log is not read, so a rewrite of its existing rows is not seen (documented " - "in GENERATE_CONTRACTS.md, Limits)", -) def test_a4_a_rewrite_of_a_log_table_on_postgresql_is_stopped( generated: dict[str, Any], ) -> None: From 64a59ee95bd12bd14ca663b23c1e1a6c398d42cc Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 21:08:43 -0400 Subject: [PATCH 80/94] Only a row named by a literal key is excused app-wide: a row the task's values name stays with its own kind --- packages/flow/openadapt_flow/generate/verify_contract.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index bb0b060b04..ff9343a199 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1078,7 +1078,8 @@ def _touched_rows( "selector": selector, "columns": kinds, "row": how, - "by_key": " row " in how, + # A literal key, not one the task's values name. + "by_key": all("literal" in v for v in selector.values()), } ) From 2b6ab77418207b4930de07662c15b0c7b4fc64bc Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 21:23:07 -0400 Subject: [PATCH 81/94] A changed row the task's values name applies only to field sets that give them --- packages/flow/openadapt_flow/generate/verify_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index ff9343a199..394ff31bab 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1869,6 +1869,12 @@ def build_contract( # values only when something is written. if found and not spec.get("by_key"): continue + # A row the task's values name applies only to a field set that + # gives them (a call logged on a contact names no lead). + if any( + "param" in v and v["param"] not in fields for v in spec["selector"].values() + ): + continue cols = [c for c in spec["columns"] if c in excused.get(spec["table"], set())] if not cols: continue From 0bd47c616bad26e5f74d623e6b9876fc180b066d Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 21:36:15 -0400 Subject: [PATCH 82/94] logs_add: any table the demonstrations added rows to, a log for every kind --- packages/flow/openadapt_flow/generate/verify_contract.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 394ff31bab..5403bd579e 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -2800,9 +2800,16 @@ def propose( candidates = set().union(*(p.log_candidates for p in plans)) if plans else set() logs = log_add if accepted else set() sides = sorted({f"{s.table} ({s.count} per write)" for p in plans for s in p.sides}) + # A table the person calls a log (one the demonstrations added rows to: + # a candidate or a counted side table) is a log for every kind, since + # an app logs every request, not only the kind a demonstration made. + added_to = candidates | {s.table for p in plans for s in p.sides} + for p in plans: + added_to |= p.logs + logs = logs & added_to for p in plans: # a table the person calls a log is no longer counted p.sides = [x for x in p.sides if x.table not in logs] - p.logs = (candidates & logs) | (p.logs & logs) + p.logs = set(logs) unexcused = {t: c for p in plans for t, c in p.unexcused.items()} if proposed or offered or unchecked or candidates or sides or unexcused: lines = [] From f4cb759f9117b81843a724c95b61a3869c97d916 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 22:36:57 -0400 Subject: [PATCH 83/94] Versions read: filter on an event code no create used (Dolibarr's COMPANY_MODIFY), codes only --- .../openadapt_flow/generate/verify_contract.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 5403bd579e..277b452967 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1195,11 +1195,18 @@ def _audit_source( continue mine = {r.get(col.name) for r in rows} theirs = {r.get(col.name) for r in created} - if len(mine) == 1 and created and len(theirs) == 1 and mine != theirs: + # One event code in every update, and in none of the creates' + # rows (COMPANY_MODIFY against COMPANY_CREATE, PRODUCT_CREATE). + # Only a code: a time stamp or a text one demonstration happened + # to share would hide other users' saves. + if len(mine) == 1 and created and mine.isdisjoint(theirs): v = next(iter(mine)) - if v is not None and (col.kind == "text" or _WHOLE_NUMBER.match(v)): - if "\\" not in v and "'" not in v and "%" not in v: - where[col.name] = v + if ( + v is not None + and col.kind == "text" + and re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,63}", v) + ): + where[col.name] = v found = { "kind": "audit", "table": name, From ea9b936cfcfbb8493f4f923c6344cfd4b872d0e7 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 22:40:32 -0400 Subject: [PATCH 84/94] Versions filter: letters-only codes, never an id column --- packages/flow/openadapt_flow/generate/verify_contract.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 277b452967..3b01fa85cd 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1197,14 +1197,16 @@ def _audit_source( theirs = {r.get(col.name) for r in created} # One event code in every update, and in none of the creates' # rows (COMPANY_MODIFY against COMPANY_CREATE, PRODUCT_CREATE). - # Only a code: a time stamp or a text one demonstration happened - # to share would hide other users' saves. + # Only a code (letters and underscores, not an id column): a time + # stamp, a token or a text the demonstrations happened to share + # would hide other users' saves. if len(mine) == 1 and created and mine.isdisjoint(theirs): v = next(iter(mine)) if ( v is not None and col.kind == "text" - and re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,63}", v) + and not col.name.lower().endswith("id") + and re.fullmatch(r"[A-Za-z_]{1,64}", v) ): where[col.name] = v found = { From f743c619288c1d3216d0ef407e4339f4f7a1ea3a Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 22:56:50 -0400 Subject: [PATCH 85/94] Final real-app results on the round-1 branch: PostgreSQL frame and recheck, logs named by the person --- CAPABILITIES.md | 2 +- llms-full.txt | 2 +- .../benchmark/generate_contracts/RESULTS.md | 77 ++- .../results/dolibarr/QUESTIONS.md | 12 +- .../results/dolibarr/answers.yaml | 4 +- ...l+name+phone+town+zip.8386ba67.create.yaml | 53 +- ...il+name+phone+town+zip.8386ba67.found.yaml | 137 +++- ...ice_date+town+linesx2.17de9df9.create.yaml | 84 +-- ...oice_date+town+linesx2.17de9df9.found.yaml | 267 +++++++- ...excl_vat+ref+vat_rate.bcd72fd0.create.yaml | 8 - ..._excl_vat+ref+vat_rate.bcd72fd0.found.yaml | 194 +++++- ...w_email+new_phone+town.110f7554.found.yaml | 6 +- ..._email+new_phone+town.110f7554.update.yaml | 9 +- .../results/dolibarr/report.json | 39 +- .../results/espocrm/QUESTIONS.md | 10 +- .../results/espocrm/answers.yaml | 4 +- ...last_name+phone+title.fef035cb.create.yaml | 163 +++-- ...+last_name+phone+title.fef035cb.found.yaml | 153 +++-- ...e+currency+name+stage.1d2f2376.create.yaml | 67 +- ...te+currency+name+stage.1d2f2376.found.yaml | 35 +- ...on+duration_minutes+s.dca67171.create.yaml | 111 ++-- ...ion+duration_minutes+s.dca67171.found.yaml | 85 ++- ...t_name+last_name+value.a43b40a4.found.yaml | 34 +- ..._name+last_name+value.a43b40a4.update.yaml | 29 +- .../results/espocrm/report.json | 105 ++- .../results/redmine/QUESTIONS.md | 8 +- .../results/redmine/answers.yaml | 5 +- ...oject+subject+tracker.d268453f.create.yaml | 579 ++-------------- ...roject+subject+tracker.d268453f.found.yaml | 620 +++--------------- ...ject+project+spent_on.a2ffb74d.create.yaml | 560 +--------------- ...bject+project+spent_on.a2ffb74d.found.yaml | 560 +--------------- ...project+status+subject.4aa4d777.found.yaml | 554 ++-------------- ...roject+status+subject.4aa4d777.update.yaml | 553 ++-------------- .../results/redmine/report.json | 94 ++- packages/flow/docs/GENERATE_CONTRACTS.md | 15 +- 35 files changed, 1678 insertions(+), 3560 deletions(-) diff --git a/CAPABILITIES.md b/CAPABILITIES.md index 1a59e9f3ce..6c9b90f141 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,7 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 4, Redmine 2, EspoCRM 3, against 11 hand decisions each; every in-scope planted fault stopped (22/22, 19/19, 21/21); correct writes confirmed 40/43, 32/38, 27/40; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/llms-full.txt b/llms-full.txt index a0cd53cf76..6a37e4042a 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -544,7 +544,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on MySQL and MariaDB, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 3, Redmine 1, EspoCRM 2, against 11 hand decisions each; every in-scope planted fault stopped; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 4, Redmine 2, EspoCRM 3, against 11 hand decisions each; every in-scope planted fault stopped (22/22, 19/19, 21/21); correct writes confirmed 40/43, 32/38, 27/40; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/packages/flow/benchmark/generate_contracts/RESULTS.md b/packages/flow/benchmark/generate_contracts/RESULTS.md index b686b7dda2..e6391c26ce 100644 --- a/packages/flow/benchmark/generate_contracts/RESULTS.md +++ b/packages/flow/benchmark/generate_contracts/RESULTS.md @@ -22,15 +22,18 @@ Late-write faults also ran the recheck the contract declares. | | Dolibarr | Redmine | EspoCRM | | --- | --- | --- | --- | | Database | MariaDB 11.4 | PostgreSQL | MariaDB | -| Questions for a person | **3** | **1** | **2** | +| Questions for a person | **4** | **2** | **3** | | First study, person decisions | 11 | 11 | 11 | -| Contracts written | 93 | 95 | 235 | -| Correct writes confirmed (tasks and controls) | 40/43 | 35/38 | 36/40 | +| Field sets with a contract pair | 93 | 95 | 235 | +| Correct writes confirmed (tasks and controls) | 40/43 | 32/38 | 27/40 | | In-scope planted faults stopped | **22/22** | **19/19** | **21/21** | | First study, in-scope faults stopped | 21/21 | 26/26 | 16/19 | -| Out-of-scope late writes stopped | 2/2 (recheck) | 0/2 (no recheck on PostgreSQL) | 1/2 (recheck) | -| Lost-update versions (`frame.versions`) | agenda events (`llx_actioncomm`) | journals | `action_history_record` | -| `propose` time | 1.5 min | 1.5 min | 2 min | +| Out-of-scope late writes stopped | 2/2 (recheck) | 1/2 (recheck) | 1/2 (recheck) | +| Lost-update versions (`frame.versions`) | agenda events (`llx_actioncomm`, `code = AC_COMPANY_MODIFY`) | journals | `action_history_record` (`action = update`, `target_type = Lead`) | +| `propose` time | 2 min | 0.5 min | 1 min | + +Every contract uses the whole-database frame (`frame.database`) with the +store's dialect, on PostgreSQL too, and declares the recheck window. The first study measured false stops on agents' writes; here the correct writes are the pack's reference writes and the controls, so the two false @@ -38,49 +41,59 @@ stop rates are not the same measure. ### Questions asked -- **Dolibarr** (3): bookkeeping (agenda events per write, `tms`, - `fk_user_modif`, the `socialnetworks` rewrite, invoice totals and draft - reference not checked); the recheck window (300 s proposed and accepted); - `invoice_date` is in `datef` and in `date_lim_reglement` (picked `datef`). -- **Redmine** (1): bookkeeping (custom field rows per write, `journal_details` - as a log, `updated_on`, `lock_version`, `user_preferences.others`, and the - time entry's `tyear`, `tmonth`, `tweek`). No recheck question: PostgreSQL - has no whole-database frame. -- **EspoCRM** (2): bookkeeping (stream notes, `call_user` and email link rows per write; `action_history_record`, `auth_log_record` and others as logs; `stream_updated_at`, `modified_at`); the recheck window (300 s). - -Every answer was the proposal, except the one choice in Dolibarr's question 3. +- **Dolibarr** (4): which parameters are phone numbers (both proposed, + both confirmed); bookkeeping (agenda events per write, `tms`, + `fk_user_modif`, invoice totals and draft reference not checked; the + person added the `socialnetworks` rewrite under `columns_add`); the recheck + window (300 s); `invoice_date` is in `datef` and in `date_lim_reglement` + (picked `datef`). +- **Redmine** (2): bookkeeping (custom field rows per write, `updated_on`, + `lock_version`, the time entry's `tyear`, `tmonth`, `tweek`; the person + named `journal_details` a log and added `user_preferences.others`); the + recheck window (300 s). +- **EspoCRM** (3): which parameters are phone numbers (confirmed); + bookkeeping (`stream_updated_at`, `modified_at`, `call_user` and stream + note rows; the person named `action_history_record` and `auth_log_record` + logs, and left `note` and `note_user` out, so stream notes are counted); + the recheck window (300 s). + +The generator decides no log by table name. Every log was named by the +person in the bookkeeping question. ### Setup steps the generator applied - Dolibarr and EspoCRM: `GRANT TRIGGER ON .* TO ` (`--grant-trigger`): the whole-database frame must see triggers. -- Before the SQL guard fix (#1665), EspoCRM also needed a view - `espocrm_oa_read.oa_read_call` (`--create-views`), because the guard - refused the table name `call`. On `main` the guard reads `call`, and the - final run created no view. +- Redmine: none. The read account needs `SELECT` on the sequences too, + which the study's fixture grants. ### False stops, by cause | App | Case | Cause | | --- | --- | --- | -| Dolibarr | F18 (control) | Another user's edit to an unrelated customer: no audit trail is read, so it isn't attributed | +| Dolibarr | F18 (control) | Another user's edit to an unrelated customer: not attributed | | Dolibarr | F21 (control) | Two saves add two agenda events; the contract expects one per write | | Dolibarr | F30 (control) | A resave of a value already in place adds an agenda event; the `found` contract allows no change | | Redmine | T19 | Closing an issue sets `closed_on`; no demonstration closed one | +| Redmine | T22, T23, T28 | The issue subject names issues in two projects: a display value must name exactly one row | | Redmine | F21 (control) | Another user's edit to an unrelated issue: not attributed | | Redmine | F24 (control) | Two saves add two journals | +| EspoCRM | T19, T23 | A status change adds a stream note; no demonstration changed the status | | EspoCRM | T21 | The reference write removes the old email address row when replacing it; the demonstration didn't | | EspoCRM | T22 | `field: phone` has no contract: no demonstration, and phones live in another table | -| EspoCRM | F22, F27 (controls) | Another user's unrelated edit or new lead: not attributed | +| EspoCRM | T29 to T34, T36 | Logging a call adds stream notes for each linked record and touches the contact's `stream_updated_at`, in numbers the demonstrations didn't show. `note` isn't a log unless the person says so | +| EspoCRM | F22, F27 (controls) | Another user's unrelated edit or new record: not attributed | -The lost-update faults are stopped with a version reason now (EspoCRM F19: -"another user saved a version of the target inside the window"); the first -study missed EspoCRM F19 and HAPI F14. +The stream-note stops are the price of not calling `note` a log: a person +who names it adds those rows to the logs, and an extra stream note (a +planted fault) would then pass. ### Misses -Out of scope only: Redmine F19 and F20 (late writes; PostgreSQL has no -recheck), and EspoCRM F21 (a late edit to a look-alike lead: an update's main table is read by its own surface, which the recheck doesn't read). No in-scope planted fault was confirmed on any app. +Out of scope only: Redmine F20 and EspoCRM F21. Both are late writes to the +main record's table, which the contract reads through its own surface; the +recheck reads the rest of the database again but not that surface. No +in-scope planted fault was confirmed on any app. ## Reproduce @@ -101,8 +114,6 @@ full set is regenerated by the commands above). The fault scripts and the oracle stay in the study's private pack. Code: the final runs used this branch rebased on `main` (with #1661 to -#1669): EspoCRM at `baf86e78a`, Dolibarr and Redmine at `c51437ba6` (the -commits after are lint, type and documentation fixes). Earlier runs on the -branch before the rebase, with #1661 merged in, gave the same counts for -Dolibarr and Redmine; for EspoCRM they missed nothing more and confirmed -one write fewer (F24, before emails compared case-folded). +#1669 and #1673): Redmine and EspoCRM at `0bd47c616`, Dolibarr at +`f4cb759f9`. The commit after narrows the versions filter to letter-only +codes; the three apps' versions reads are the same under it. diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md index 336f371750..8bf6405a80 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md @@ -1,14 +1,18 @@ # Questions for a person -1. Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): llx_actioncomm (1 per write), llx_actioncomm_resources (1 per write). Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. +1. These values were stored as other text with the same digits (a phone number written with spaces stored without): create_customer.phone, update_customer.new_phone. Compare them as phone numbers (the same digits, at least 7, and a task value that isn't shaped like a phone number is compared exactly)? Answer `phones: [, ...]` with the ones that are phone numbers; any other is compared exactly. + - Answer key: `phones`. Proposed: `['create_customer.phone', 'update_customer.new_phone']`. + - Answered: `["create_customer.phone", "update_customer.new_phone"]` + +2. Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (llx_societe: the target row): llx_societe: fk_user_modif, tms. Also changed with no task value, and not excused unless you add them under columns_add (a write that changes them stops): llx_societe: socialnetworks. Rows the app adds beside each write, expected exactly that many times: llx_actioncomm (1 per write), llx_actioncomm_resources (1 per write). Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): llx_facture: close_code, close_note, date_lim_reglement, datec, extraparams, fk_currency, import_key, increment, last_main_doc, location_incoterms, model_pdf, module_source, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, note_private, note_public, payment_reference, pos_source, ref, ref_client, ref_ext, tms, total_ht, total_ttc, total_tva; llx_facturedet: batch, description, import_key, label, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref_ext, total_ht, total_ttc, total_tva, vat_src_code; llx_product: accountancy_code_buy, accountancy_code_buy_export, accountancy_code_buy_intra, accountancy_code_sell, accountancy_code_sell_export, accountancy_code_sell_intra, barcode, batch_mask, canvas, customcode, datec, default_vat_code, description, duration, import_key, last_main_doc, model_pdf, note, note_public, partnumber, price_label, price_ttc, ref_ext, tms, url; llx_product_price: date_price, default_vat_code, import_key, multicurrency_code, price_label, price_ttc, tms; llx_societe: accountancy_code_buy, accountancy_code_customer_general, accountancy_code_sell, accountancy_code_supplier_general, ape, barcode, canvas, code_client, code_compta, code_compta_fournisseur, code_fournisseur, datec, default_lang, deposit_percent, fax, fk_currency, fk_prospectlevel, georesultcode, idprof4, idprof5, idprof6, import_key, ip, last_main_doc, location_incoterms, logo, logo_squarred, model_pdf, multicurrency_code, name_alias, note_private, note_public, phone_mobile, prefix_comm, ref_ext, siren, siret, supplier_account, tms, tp_payment_reference, tva_intra, url, webservices_key, webservices_url. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log. - Answer key: `bookkeeping`. Proposed: `accept`. - - Answered: `"accept"` + - Answered: `{"columns_add": {"llx_societe": ["socialnetworks"]}}` -2. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. +3. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. - Answer key: `recheck_after_s`. Proposed: `300`. - Answered: `300` -3. Kind `create_draft_invoice`: `invoice_date` is in several places: [0] llx_facture.datef (date); [1] llx_facture.date_lim_reglement (date) Answer `locations.create_draft_invoice: {: }` to pick one. +4. Kind `create_draft_invoice`: `invoice_date` is in several places: [0] llx_facture.datef (date); [1] llx_facture.date_lim_reglement (date) Answer `locations.create_draft_invoice: {: }` to pick one. - Answer key: `locations.create_draft_invoice`. Proposed: `None`. - Answered: `{"invoice_date": 0}` diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml index d81a017c37..01984d2f3d 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml @@ -1,3 +1,5 @@ -bookkeeping: accept +phones: [create_customer.phone, update_customer.new_phone] +bookkeeping: + columns_add: {llx_societe: [socialnetworks]} recheck_after_s: 300 locations.create_draft_invoice: {invoice_date: 0} diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml index bf2b55e636..93e782d621 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml @@ -61,30 +61,31 @@ verifier: CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key`, (SELECT CAST(a1.`label` - AS CHAR) FROM `llx_c_country` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_pays` AS BINARY)) - AS `p__country`, CASE WHEN t.`capital` IS NULL THEN '1' ELSE '0' END AS `pin__capital`, CASE WHEN - t.`client` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__client`, CASE WHEN t.`cond_reglement` - IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement`, CASE WHEN t.`cond_reglement_supplier` - IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement_supplier`, CASE WHEN t.`customer_bad` = - CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_bad`, CASE WHEN t.`customer_rate` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_rate`, CASE WHEN t.`entity` - = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fk_account` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_barcode_type` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, CASE WHEN t.`fk_departement` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_departement`, CASE WHEN t.`fk_effectif` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_effectif`, CASE WHEN t.`fk_forme_juridique` IS NULL THEN '1' ELSE - '0' END AS `pin__fk_forme_juridique`, CASE WHEN t.`fk_incoterms` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fk_incoterms`, CASE WHEN t.`fk_multicurrency` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_shipping_method` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_shipping_method`, CASE WHEN t.`fk_stcomm` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fk_stcomm`, CASE WHEN t.`fk_typent` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fk_typent`, CASE WHEN t.`fk_warehouse` IS NULL THEN '1' ELSE '0' - END AS `pin__fk_warehouse`, CASE WHEN t.`fournisseur` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE - '0' END AS `pin__fournisseur`, CASE WHEN t.`geolat` IS NULL THEN '1' ELSE '0' END AS `pin__geolat`, - CASE WHEN t.`geolong` IS NULL THEN '1' ELSE '0' END AS `pin__geolong`, CASE WHEN t.`geopoint` - IS NULL THEN '1' ELSE '0' END AS `pin__geopoint`, CASE WHEN t.`localtax1_assuj` = CAST('0' AS - DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_assuj`, CASE WHEN t.`localtax1_value` + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key`, (SELECT CASE WHEN + (SELECT COUNT(*) FROM `llx_c_country` u1 WHERE CAST(CAST(u1.`label` AS CHAR) AS BINARY) = CAST(CAST(a1.`label` + AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`label` AS CHAR) END FROM `llx_c_country` a1 WHERE CAST(a1.`rowid` + AS BINARY) = CAST(t.`fk_pays` AS BINARY)) AS `p__country`, CASE WHEN t.`capital` IS NULL THEN + '1' ELSE '0' END AS `pin__capital`, CASE WHEN t.`client` = CAST('1' AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `pin__client`, CASE WHEN t.`cond_reglement` IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement`, + CASE WHEN t.`cond_reglement_supplier` IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement_supplier`, + CASE WHEN t.`customer_bad` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_bad`, + CASE WHEN t.`customer_rate` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_rate`, + CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE + WHEN t.`fk_account` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_account`, + CASE WHEN t.`fk_barcode_type` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, + CASE WHEN t.`fk_departement` IS NULL THEN '1' ELSE '0' END AS `pin__fk_departement`, CASE WHEN + t.`fk_effectif` IS NULL THEN '1' ELSE '0' END AS `pin__fk_effectif`, CASE WHEN t.`fk_forme_juridique` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_forme_juridique`, CASE WHEN t.`fk_incoterms` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_incoterms`, CASE WHEN t.`fk_multicurrency` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_shipping_method` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_shipping_method`, CASE WHEN t.`fk_stcomm` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_stcomm`, CASE WHEN t.`fk_typent` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_typent`, CASE WHEN t.`fk_warehouse` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`fournisseur` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fournisseur`, CASE WHEN t.`geolat` IS NULL THEN '1' ELSE '0' END + AS `pin__geolat`, CASE WHEN t.`geolong` IS NULL THEN '1' ELSE '0' END AS `pin__geolong`, CASE + WHEN t.`geopoint` IS NULL THEN '1' ELSE '0' END AS `pin__geopoint`, CASE WHEN t.`localtax1_assuj` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_assuj`, CASE WHEN t.`localtax1_value` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_value`, CASE WHEN t.`localtax2_assuj` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_assuj`, CASE WHEN t.`localtax2_value` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_value`, CASE WHEN t.`mode_reglement` @@ -294,10 +295,6 @@ effects: expected_count: 1 count_new_only: true frame: - ignore_fields: - - fk_user_modif - - socialnetworks - - tms target_fields: [] database: dialect: mysql diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml index c00bc365fc..1e227bba96 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml @@ -61,9 +61,50 @@ verifier: CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key`, (SELECT CAST(a1.`label` - AS CHAR) FROM `llx_c_country` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_pays` AS BINARY)) - AS `p__country` FROM `llx_societe` t + CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key`, (SELECT CASE WHEN + (SELECT COUNT(*) FROM `llx_c_country` u1 WHERE CAST(CAST(u1.`label` AS CHAR) AS BINARY) = CAST(CAST(a1.`label` + AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`label` AS CHAR) END FROM `llx_c_country` a1 WHERE CAST(a1.`rowid` + AS BINARY) = CAST(t.`fk_pays` AS BINARY)) AS `p__country`, CASE WHEN t.`capital` IS NULL THEN + '1' ELSE '0' END AS `pin__capital`, CASE WHEN t.`client` = CAST('1' AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `pin__client`, CASE WHEN t.`cond_reglement` IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement`, + CASE WHEN t.`cond_reglement_supplier` IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement_supplier`, + CASE WHEN t.`customer_bad` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_bad`, + CASE WHEN t.`customer_rate` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_rate`, + CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE + WHEN t.`fk_account` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_account`, + CASE WHEN t.`fk_barcode_type` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, + CASE WHEN t.`fk_departement` IS NULL THEN '1' ELSE '0' END AS `pin__fk_departement`, CASE WHEN + t.`fk_effectif` IS NULL THEN '1' ELSE '0' END AS `pin__fk_effectif`, CASE WHEN t.`fk_forme_juridique` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_forme_juridique`, CASE WHEN t.`fk_incoterms` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_incoterms`, CASE WHEN t.`fk_multicurrency` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_shipping_method` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_shipping_method`, CASE WHEN t.`fk_stcomm` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_stcomm`, CASE WHEN t.`fk_typent` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_typent`, CASE WHEN t.`fk_warehouse` IS NULL + THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`fournisseur` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fournisseur`, CASE WHEN t.`geolat` IS NULL THEN '1' ELSE '0' END + AS `pin__geolat`, CASE WHEN t.`geolong` IS NULL THEN '1' ELSE '0' END AS `pin__geolong`, CASE + WHEN t.`geopoint` IS NULL THEN '1' ELSE '0' END AS `pin__geopoint`, CASE WHEN t.`localtax1_assuj` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_assuj`, CASE WHEN t.`localtax1_value` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_value`, CASE WHEN t.`localtax2_assuj` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_assuj`, CASE WHEN t.`localtax2_value` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_value`, CASE WHEN t.`mode_reglement` + IS NULL THEN '1' ELSE '0' END AS `pin__mode_reglement`, CASE WHEN t.`mode_reglement_supplier` + IS NULL THEN '1' ELSE '0' END AS `pin__mode_reglement_supplier`, CASE WHEN t.`order_min_amount` + IS NULL THEN '1' ELSE '0' END AS `pin__order_min_amount`, CASE WHEN t.`outstanding_limit` IS NULL + THEN '1' ELSE '0' END AS `pin__outstanding_limit`, CASE WHEN t.`parent` IS NULL THEN '1' ELSE + '0' END AS `pin__parent`, CASE WHEN t.`price_level` IS NULL THEN '1' ELSE '0' END AS `pin__price_level`, + CASE WHEN t.`remise_client` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_client`, + CASE WHEN t.`remise_supplier` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_supplier`, + CASE WHEN t.`status` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__status`, CASE + WHEN t.`statut` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__statut`, CASE WHEN + t.`supplier_order_min_amount` IS NULL THEN '1' ELSE '0' END AS `pin__supplier_order_min_amount`, + CASE WHEN t.`supplier_rate` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__supplier_rate`, + CASE WHEN t.`transport_mode` IS NULL THEN '1' ELSE '0' END AS `pin__transport_mode`, CASE WHEN + t.`transport_mode_supplier` IS NULL THEN '1' ELSE '0' END AS `pin__transport_mode_supplier`, CASE + WHEN t.`tva_assuj` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tva_assuj`, CASE + WHEN t.`vat_reverse_charge` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__vat_reverse_charge` + FROM `llx_societe` t effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -84,12 +125,94 @@ effects: param: town zip: param: zip + pin__capital: + literal: '1' + pin__client: + literal: '1' + pin__cond_reglement: + literal: '1' + pin__cond_reglement_supplier: + literal: '1' + pin__customer_bad: + literal: '1' + pin__customer_rate: + literal: '1' + pin__entity: + literal: '1' + pin__fk_account: + literal: '1' + pin__fk_barcode_type: + literal: '1' + pin__fk_departement: + literal: '1' + pin__fk_effectif: + literal: '1' + pin__fk_forme_juridique: + literal: '1' + pin__fk_incoterms: + literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_shipping_method: + literal: '1' + pin__fk_stcomm: + literal: '1' + pin__fk_typent: + literal: '1' + pin__fk_warehouse: + literal: '1' + pin__fournisseur: + literal: '1' + pin__geolat: + literal: '1' + pin__geolong: + literal: '1' + pin__geopoint: + literal: '1' + pin__localtax1_assuj: + literal: '1' + pin__localtax1_value: + literal: '1' + pin__localtax2_assuj: + literal: '1' + pin__localtax2_value: + literal: '1' + pin__mode_reglement: + literal: '1' + pin__mode_reglement_supplier: + literal: '1' + pin__order_min_amount: + literal: '1' + pin__outstanding_limit: + literal: '1' + pin__parent: + literal: '1' + pin__price_level: + literal: '1' + pin__remise_client: + literal: '1' + pin__remise_supplier: + literal: '1' + socialnetworks: + literal: 'null' + pin__status: + literal: '1' + pin__statut: + literal: '1' + pin__supplier_order_min_amount: + literal: '1' + pin__supplier_rate: + literal: '1' + pin__transport_mode: + literal: '1' + pin__transport_mode_supplier: + literal: '1' + pin__tva_assuj: + literal: '1' + pin__vat_reverse_charge: + literal: '1' expected_count: 1 frame: - ignore_fields: - - fk_user_modif - - socialnetworks - - tms target_fields: [] database: dialect: mysql diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml index f2931d37f3..1a4033f636 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml @@ -49,13 +49,18 @@ verifier: AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, - CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` - AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, CASE WHEN t.`datef` - = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, (SELECT CAST(a1.`town` AS CHAR) - FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town`, CASE WHEN t.`close_missing_amount` - IS NULL THEN '1' ELSE '0' END AS `pin__close_missing_amount`, CASE WHEN t.`date_closing` IS NULL - THEN '1' ELSE '0' END AS `pin__date_closing`, CASE WHEN t.`date_pointoftax` IS NULL THEN '1' ELSE - '0' END AS `pin__date_pointoftax`, CASE WHEN t.`date_valid` IS NULL THEN '1' ELSE '0' END AS `pin__date_valid`, + CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CASE WHEN (SELECT + COUNT(*) FROM `llx_societe` u1 WHERE CAST(CAST(u1.`nom` AS CHAR) AS BINARY) = CAST(CAST(a1.`nom` + AS CHAR) AS BINARY) AND CAST(CAST(u1.`town` AS CHAR) AS BINARY) = CAST(CAST(a1.`town` AS CHAR) + AS BINARY)) = 1 THEN CAST(a1.`nom` AS CHAR) END FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) + AS `p__customer`, CASE WHEN t.`datef` = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, + (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` u1 WHERE CAST(CAST(u1.`nom` AS CHAR) AS + BINARY) = CAST(CAST(a1.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u1.`town` AS CHAR) AS BINARY) = + CAST(CAST(a1.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`town` AS CHAR) END FROM `llx_societe` + a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town`, CASE WHEN t.`close_missing_amount` IS NULL THEN + '1' ELSE '0' END AS `pin__close_missing_amount`, CASE WHEN t.`date_closing` IS NULL THEN '1' ELSE + '0' END AS `pin__date_closing`, CASE WHEN t.`date_pointoftax` IS NULL THEN '1' ELSE '0' END AS + `pin__date_pointoftax`, CASE WHEN t.`date_valid` IS NULL THEN '1' ELSE '0' END AS `pin__date_valid`, CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fk_account` IS NULL THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_cond_reglement` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_cond_reglement`, CASE WHEN t.`fk_fac_rec_source` @@ -122,28 +127,34 @@ verifier: AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CAST(a2.`nom` AS CHAR) FROM `llx_societe` + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` + u2 WHERE CAST(CAST(u2.`nom` AS CHAR) AS BINARY) = CAST(CAST(a2.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u2.`town` + AS CHAR) AS BINARY) = CAST(CAST(a2.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a2.`nom` AS CHAR) + END FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` + = t.`fk_facture`) AS `main__customer`, CASE WHEN (SELECT a1.`datef` FROM `llx_facture` a1 WHERE + a1.`rowid` = t.`fk_facture`) = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `main__invoice_date`, + (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` u2 WHERE CAST(CAST(u2.`nom` AS CHAR) + AS BINARY) = CAST(CAST(a2.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u2.`town` AS CHAR) AS BINARY) + = CAST(CAST(a2.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a2.`town` AS CHAR) END FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS - `main__customer`, CASE WHEN (SELECT a1.`datef` FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) - = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `main__invoice_date`, (SELECT (SELECT CAST(a2.`town` - AS CHAR) FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` - = t.`fk_facture`) AS `main__town`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE - CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_0_product_ref`, CASE - WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v1)s AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `p__lines_0_qty`, CASE WHEN t.`buy_price_ht` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE WHEN t.`date_end` IS NULL THEN '1' ELSE '0' - END AS `pin__date_end`, CASE WHEN t.`date_start` IS NULL THEN '1' ELSE '0' END AS `pin__date_start`, - CASE WHEN t.`fk_code_ventilation` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, - CASE WHEN t.`fk_contract_line` IS NULL THEN '1' ELSE '0' END AS `pin__fk_contract_line`, CASE - WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_facture` s WHERE s.`rowid` - = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_parent_line` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_prev_id`, CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, CASE WHEN t.`fk_remise_except` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE - '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` FROM `llx_facture` - s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, CASE WHEN - t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_user_modif`, + `main__town`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_product` u1 WHERE CAST(CAST(u1.`ref` + AS CHAR) AS BINARY) = CAST(CAST(a1.`ref` AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`ref` AS CHAR) + END FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS + `p__lines_0_product_ref`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` + = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_0_qty`, CASE WHEN t.`buy_price_ht` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE WHEN t.`date_end` + IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS NULL THEN '1' ELSE + '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` + FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, + CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE '0' END AS `pin__fk_parent_line`, CASE WHEN + t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, CASE WHEN t.`fk_product_fournisseur_price` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, CASE WHEN t.`fk_remise_except` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE WHEN t.`fk_unit` IS NULL THEN '1' + ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` FROM + `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, + CASE WHEN t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__info_bits`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_tx`, @@ -167,10 +178,11 @@ verifier: CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax2`, CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx`, (SELECT - CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` - AS BINARY)) AS `p__lines_1_product_ref`, CASE WHEN CAST(%(v2)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_1_qty` FROM `llx_facturedet` - t + CASE WHEN (SELECT COUNT(*) FROM `llx_product` u1 WHERE CAST(CAST(u1.`ref` AS CHAR) AS BINARY) + = CAST(CAST(a1.`ref` AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`ref` AS CHAR) END FROM `llx_product` + a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, + CASE WHEN CAST(%(v2)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `p__lines_1_qty` FROM `llx_facturedet` t sql_query_params: v0: *id002 v1: @@ -432,9 +444,6 @@ effects: expected_count: 1 count_new_only: true frame: - ignore_fields: - - fk_user_modif - - tms target_fields: [] database: dialect: mysql @@ -448,9 +457,4 @@ frame: views: {} settle_s: 1 settle_attempts: 5 - ignore_columns: - llx_societe: - - fk_user_modif - - socialnetworks - - tms recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml index 7db480bfb8..44dbe49ddd 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml @@ -49,10 +49,46 @@ verifier: AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, - CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CAST(a1.`nom` - AS CHAR) FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__customer`, CASE WHEN t.`datef` - = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, (SELECT CAST(a1.`town` AS CHAR) - FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town` FROM `llx_facture` t + CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CASE WHEN (SELECT + COUNT(*) FROM `llx_societe` u1 WHERE CAST(CAST(u1.`nom` AS CHAR) AS BINARY) = CAST(CAST(a1.`nom` + AS CHAR) AS BINARY) AND CAST(CAST(u1.`town` AS CHAR) AS BINARY) = CAST(CAST(a1.`town` AS CHAR) + AS BINARY)) = 1 THEN CAST(a1.`nom` AS CHAR) END FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) + AS `p__customer`, CASE WHEN t.`datef` = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, + (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` u1 WHERE CAST(CAST(u1.`nom` AS CHAR) AS + BINARY) = CAST(CAST(a1.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u1.`town` AS CHAR) AS BINARY) = + CAST(CAST(a1.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`town` AS CHAR) END FROM `llx_societe` + a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town`, CASE WHEN t.`close_missing_amount` IS NULL THEN + '1' ELSE '0' END AS `pin__close_missing_amount`, CASE WHEN t.`date_closing` IS NULL THEN '1' ELSE + '0' END AS `pin__date_closing`, CASE WHEN t.`date_pointoftax` IS NULL THEN '1' ELSE '0' END AS + `pin__date_pointoftax`, CASE WHEN t.`date_valid` IS NULL THEN '1' ELSE '0' END AS `pin__date_valid`, + CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE + WHEN t.`fk_account` IS NULL THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_cond_reglement` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_cond_reglement`, CASE WHEN t.`fk_fac_rec_source` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_fac_rec_source`, CASE WHEN t.`fk_facture_source` IS + NULL THEN '1' ELSE '0' END AS `pin__fk_facture_source`, CASE WHEN t.`fk_mode_reglement` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_mode_reglement`, CASE WHEN t.`fk_projet` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_projet`, CASE WHEN t.`fk_transport_mode` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_transport_mode`, CASE WHEN t.`fk_user_closing` IS NULL THEN '1' ELSE + '0' END AS `pin__fk_user_closing`, CASE WHEN t.`fk_user_modif` IS NULL THEN '1' ELSE '0' END AS + `pin__fk_user_modif`, CASE WHEN t.`fk_user_valid` IS NULL THEN '1' ELSE '0' END AS `pin__fk_user_valid`, + CASE WHEN t.`is_also_delivery_note` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__is_also_delivery_note`, + CASE WHEN t.`localtax1` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1`, + CASE WHEN t.`localtax2` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2`, + CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, + CASE WHEN t.`paye` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__paye`, CASE WHEN + t.`prorata_discount` IS NULL THEN '1' ELSE '0' END AS `pin__prorata_discount`, CASE WHEN t.`remise` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise`, CASE WHEN t.`remise_absolue` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_absolue`, CASE WHEN t.`remise_percent` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`retained_warranty` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty`, CASE WHEN t.`retained_warranty_date_limit` + IS NULL THEN '1' ELSE '0' END AS `pin__retained_warranty_date_limit`, CASE WHEN t.`retained_warranty_fk_cond_reglement` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty_fk_cond_reglement`, + CASE WHEN t.`revenuestamp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__revenuestamp`, + CASE WHEN t.`situation_counter` IS NULL THEN '1' ELSE '0' END AS `pin__situation_counter`, CASE + WHEN t.`situation_cycle_ref` IS NULL THEN '1' ELSE '0' END AS `pin__situation_cycle_ref`, CASE + WHEN t.`situation_final` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_final`, + CASE WHEN t.`subtype` IS NULL THEN '1' ELSE '0' END AS `pin__subtype`, CASE WHEN t.`type` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__type` FROM `llx_facture` t sql_query_params: v0: &id002 param: invoice_date @@ -86,18 +122,47 @@ verifier: AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CAST(a2.`nom` AS CHAR) FROM `llx_societe` + CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` + u2 WHERE CAST(CAST(u2.`nom` AS CHAR) AS BINARY) = CAST(CAST(a2.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u2.`town` + AS CHAR) AS BINARY) = CAST(CAST(a2.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a2.`nom` AS CHAR) + END FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` + = t.`fk_facture`) AS `main__customer`, CASE WHEN (SELECT a1.`datef` FROM `llx_facture` a1 WHERE + a1.`rowid` = t.`fk_facture`) = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `main__invoice_date`, + (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` u2 WHERE CAST(CAST(u2.`nom` AS CHAR) + AS BINARY) = CAST(CAST(a2.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u2.`town` AS CHAR) AS BINARY) + = CAST(CAST(a2.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a2.`town` AS CHAR) END FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS - `main__customer`, CASE WHEN (SELECT a1.`datef` FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) - = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `main__invoice_date`, (SELECT (SELECT CAST(a2.`town` - AS CHAR) FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` - = t.`fk_facture`) AS `main__town`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE - CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_0_product_ref`, CASE - WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v1)s AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `p__lines_0_qty`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 - WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, - CASE WHEN CAST(%(v2)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `p__lines_1_qty` FROM `llx_facturedet` t + `main__town`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_product` u1 WHERE CAST(CAST(u1.`ref` + AS CHAR) AS BINARY) = CAST(CAST(a1.`ref` AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`ref` AS CHAR) + END FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS + `p__lines_0_product_ref`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` + = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_0_qty`, CASE WHEN t.`buy_price_ht` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE WHEN t.`date_end` + IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS NULL THEN '1' ELSE + '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` IS NULL THEN + '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE + '0' END AS `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, + CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, + CASE WHEN t.`fk_remise_except` IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE + WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_modif` + = CAST('2' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_warehouse` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`info_bits` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__info_bits`, CASE WHEN t.`localtax1_tx` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`multicurrency_subprice_ttc` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE + WHEN t.`price` IS NULL THEN '1' ELSE '0' END AS `pin__price`, CASE WHEN t.`rang` = CAST('0' AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN t.`remise_percent` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`situation_percent` + = CAST('100' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_percent`, CASE WHEN t.`special_code` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__special_code`, CASE WHEN t.`subprice_ttc` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__subprice_ttc`, (SELECT CASE WHEN + (SELECT COUNT(*) FROM `llx_product` u1 WHERE CAST(CAST(u1.`ref` AS CHAR) AS BINARY) = CAST(CAST(a1.`ref` + AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`ref` AS CHAR) END FROM `llx_product` a1 WHERE CAST(a1.`rowid` + AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, CASE WHEN CAST(%(v2)s + AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `p__lines_1_qty` FROM `llx_facturedet` t sql_query_params: v0: *id002 v1: @@ -114,6 +179,74 @@ effects: literal: '1' p__town: param: town + pin__close_missing_amount: + literal: '1' + pin__date_closing: + literal: '1' + pin__date_pointoftax: + literal: '1' + pin__date_valid: + literal: '1' + pin__entity: + literal: '1' + pin__fk_account: + literal: '1' + pin__fk_cond_reglement: + literal: '1' + pin__fk_fac_rec_source: + literal: '1' + pin__fk_facture_source: + literal: '1' + pin__fk_mode_reglement: + literal: '1' + pin__fk_projet: + literal: '1' + pin__fk_transport_mode: + literal: '1' + pin__fk_user_closing: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_user_valid: + literal: '1' + pin__is_also_delivery_note: + literal: '1' + pin__localtax1: + literal: '1' + pin__localtax2: + literal: '1' + multicurrency_code: + literal: EUR + pin__multicurrency_tx: + literal: '1' + pin__paye: + literal: '1' + pin__prorata_discount: + literal: '1' + pin__remise: + literal: '1' + pin__remise_absolue: + literal: '1' + pin__remise_percent: + literal: '1' + pin__retained_warranty: + literal: '1' + pin__retained_warranty_date_limit: + literal: '1' + pin__retained_warranty_fk_cond_reglement: + literal: '1' + pin__revenuestamp: + literal: '1' + pin__situation_counter: + literal: '1' + pin__situation_cycle_ref: + literal: '1' + pin__situation_final: + literal: '1' + pin__subtype: + literal: '1' + pin__type: + literal: '1' expected_count: 1 - kind: record_written probe: surface=rows_llx_facturedet|a row beside the main record holds lines @@ -128,6 +261,54 @@ effects: param: lines.0.product_ref p__lines_0_qty: literal: '1' + pin__buy_price_ht: + literal: '1' + pin__date_end: + literal: '1' + pin__date_start: + literal: '1' + pin__fk_code_ventilation: + literal: '1' + pin__fk_contract_line: + literal: '1' + pin__fk_parent_line: + literal: '1' + pin__fk_prev_id: + literal: '1' + pin__fk_product_fournisseur_price: + literal: '1' + pin__fk_remise_except: + literal: '1' + pin__fk_unit: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_warehouse: + literal: '1' + pin__info_bits: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__multicurrency_subprice_ttc: + literal: '1' + pin__price: + literal: '1' + pin__rang: + literal: '1' + pin__remise_percent: + literal: '1' + pin__situation_percent: + literal: '1' + pin__special_code: + literal: '1' + pin__subprice_ttc: + literal: '1' expected_count: 1 - kind: record_written probe: surface=rows_llx_facturedet|a row beside the main record holds lines @@ -142,11 +323,56 @@ effects: param: lines.1.product_ref p__lines_1_qty: literal: '1' + pin__buy_price_ht: + literal: '1' + pin__date_end: + literal: '1' + pin__date_start: + literal: '1' + pin__fk_code_ventilation: + literal: '1' + pin__fk_contract_line: + literal: '1' + pin__fk_parent_line: + literal: '1' + pin__fk_prev_id: + literal: '1' + pin__fk_product_fournisseur_price: + literal: '1' + pin__fk_remise_except: + literal: '1' + pin__fk_unit: + literal: '1' + pin__fk_user_modif: + literal: '1' + pin__fk_warehouse: + literal: '1' + pin__info_bits: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__multicurrency_subprice_ttc: + literal: '1' + pin__price: + literal: '1' + pin__rang: + literal: '1' + pin__remise_percent: + literal: '1' + pin__situation_percent: + literal: '1' + pin__special_code: + literal: '1' + pin__subprice_ttc: + literal: '1' expected_count: 1 frame: - ignore_fields: - - fk_user_modif - - tms target_fields: [] database: dialect: mysql @@ -160,9 +386,4 @@ frame: views: {} settle_s: 1 settle_attempts: 5 - ignore_columns: - llx_societe: - - fk_user_modif - - socialnetworks - - tms recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml index 32e4406879..2add8b62c7 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml @@ -475,9 +475,6 @@ effects: expected_count: 1 count_new_only: true frame: - ignore_fields: - - fk_user_modif - - tms target_fields: [] database: dialect: mysql @@ -528,9 +525,4 @@ frame: - fk_actioncomm - rowid - transparency - ignore_columns: - llx_societe: - - fk_user_modif - - socialnetworks - - tms recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml index 3be453e4a0..79d8c6f07e 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml @@ -58,8 +58,51 @@ verifier: AS `mandatory_period`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__price_excl_vat`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate` FROM `llx_product` - t + AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate`, CASE WHEN + t.`cost_price` IS NULL THEN '1' ELSE '0' END AS `pin__cost_price`, CASE WHEN t.`desiredstock` + = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__desiredstock`, CASE WHEN t.`entity` + = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fifo` IS NULL + THEN '1' ELSE '0' END AS `pin__fifo`, CASE WHEN t.`finished` IS NULL THEN '1' ELSE '0' END AS + `pin__finished`, CASE WHEN t.`fk_barcode_type` IS NULL THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, + CASE WHEN t.`fk_country` IS NULL THEN '1' ELSE '0' END AS `pin__fk_country`, CASE WHEN t.`fk_default_bom` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_default_bom`, CASE WHEN t.`fk_default_warehouse` IS + NULL THEN '1' ELSE '0' END AS `pin__fk_default_warehouse`, CASE WHEN t.`fk_default_workstation` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_default_workstation`, CASE WHEN t.`fk_parent` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_parent`, CASE WHEN t.`fk_price_expression` + IS NULL THEN '1' ELSE '0' END AS `pin__fk_price_expression`, CASE WHEN t.`fk_product_type` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_product_type`, CASE WHEN t.`fk_project` IS + NULL THEN '1' ELSE '0' END AS `pin__fk_project`, CASE WHEN t.`fk_state` IS NULL THEN '1' ELSE + '0' END AS `pin__fk_state`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, + CASE WHEN t.`height` IS NULL THEN '1' ELSE '0' END AS `pin__height`, CASE WHEN t.`height_units` + IS NULL THEN '1' ELSE '0' END AS `pin__height_units`, CASE WHEN t.`hidden` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__hidden`, CASE WHEN t.`length` IS NULL THEN '1' ELSE '0' END AS + `pin__length`, CASE WHEN t.`length_units` IS NULL THEN '1' ELSE '0' END AS `pin__length_units`, + CASE WHEN t.`lifetime` IS NULL THEN '1' ELSE '0' END AS `pin__lifetime`, CASE WHEN t.`lifo` IS + NULL THEN '1' ELSE '0' END AS `pin__lifo`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`mandatory_period` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__mandatory_period`, CASE WHEN t.`net_measure` IS NULL THEN '1' ELSE + '0' END AS `pin__net_measure`, CASE WHEN t.`net_measure_units` IS NULL THEN '1' ELSE '0' END AS + `pin__net_measure_units`, CASE WHEN t.`pmp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__pmp`, CASE WHEN t.`price_autogen` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__price_autogen`, CASE WHEN t.`price_min` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' + END AS `pin__price_min`, CASE WHEN t.`price_min_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE + '0' END AS `pin__price_min_ttc`, CASE WHEN t.`qc_frequency` IS NULL THEN '1' ELSE '0' END AS `pin__qc_frequency`, + CASE WHEN t.`recuperableonly` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__recuperableonly`, + CASE WHEN t.`sell_or_eat_by_mandatory` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `pin__sell_or_eat_by_mandatory`, CASE WHEN t.`seuil_stock_alerte` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__seuil_stock_alerte`, CASE WHEN t.`stock` IS NULL THEN '1' ELSE + '0' END AS `pin__stock`, CASE WHEN t.`stockable_product` = CAST('1' AS DECIMAL(65,30)) THEN '1' + ELSE '0' END AS `pin__stockable_product`, CASE WHEN t.`surface` IS NULL THEN '1' ELSE '0' END + AS `pin__surface`, CASE WHEN t.`surface_units` IS NULL THEN '1' ELSE '0' END AS `pin__surface_units`, + CASE WHEN t.`tobatch` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tobatch`, CASE + WHEN t.`tobuy` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tobuy`, CASE WHEN + t.`tosell` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tosell`, CASE WHEN t.`volume` + IS NULL THEN '1' ELSE '0' END AS `pin__volume`, CASE WHEN t.`volume_units` IS NULL THEN '1' ELSE + '0' END AS `pin__volume_units`, CASE WHEN t.`weight` IS NULL THEN '1' ELSE '0' END AS `pin__weight`, + CASE WHEN t.`weight_units` IS NULL THEN '1' ELSE '0' END AS `pin__weight_units`, CASE WHEN t.`width` + IS NULL THEN '1' ELSE '0' END AS `pin__width`, CASE WHEN t.`width_units` IS NULL THEN '1' ELSE + '0' END AS `pin__width_units` FROM `llx_product` t sql_query_params: v0: &id002 param: price_excl_vat @@ -93,9 +136,14 @@ verifier: AND (SELECT a1.`tva_tx` FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__vat_rate`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `p__price_excl_vat`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`tva_tx` - = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate` FROM `llx_product_price` - t + AS `p__price_excl_vat`, CASE WHEN t.`fk_multicurrency` IS NULL THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, + CASE WHEN t.`fk_price_expression` IS NULL THEN '1' ELSE '0' END AS `pin__fk_price_expression`, + CASE WHEN t.`multicurrency_price` IS NULL THEN '1' ELSE '0' END AS `pin__multicurrency_price`, + CASE WHEN t.`multicurrency_price_ttc` IS NULL THEN '1' ELSE '0' END AS `pin__multicurrency_price_ttc`, + CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, + CASE WHEN t.`price_min_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__price_min_ttc`, + CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`tva_tx` = CAST(%(v1)s AS + DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate` FROM `llx_product_price` t sql_query_params: v0: *id002 v1: *id003 @@ -111,6 +159,110 @@ effects: param: ref p__vat_rate: literal: '1' + pin__cost_price: + literal: '1' + pin__desiredstock: + literal: '1' + pin__entity: + literal: '1' + pin__fifo: + literal: '1' + pin__finished: + literal: '1' + pin__fk_barcode_type: + literal: '1' + pin__fk_country: + literal: '1' + pin__fk_default_bom: + literal: '1' + pin__fk_default_warehouse: + literal: '1' + pin__fk_default_workstation: + literal: '1' + pin__fk_parent: + literal: '1' + pin__fk_price_expression: + literal: '1' + pin__fk_product_type: + literal: '1' + pin__fk_project: + literal: '1' + pin__fk_state: + literal: '1' + pin__fk_unit: + literal: '1' + pin__height: + literal: '1' + pin__height_units: + literal: '1' + pin__hidden: + literal: '1' + pin__length: + literal: '1' + pin__length_units: + literal: '1' + pin__lifetime: + literal: '1' + pin__lifo: + literal: '1' + pin__localtax1_tx: + literal: '1' + localtax1_type: + literal: '0' + pin__localtax2_tx: + literal: '1' + localtax2_type: + literal: '0' + pin__mandatory_period: + literal: '1' + pin__net_measure: + literal: '1' + pin__net_measure_units: + literal: '1' + pin__pmp: + literal: '1' + pin__price_autogen: + literal: '1' + price_base_type: + literal: HT + pin__price_min: + literal: '1' + pin__price_min_ttc: + literal: '1' + pin__qc_frequency: + literal: '1' + pin__recuperableonly: + literal: '1' + pin__sell_or_eat_by_mandatory: + literal: '1' + pin__seuil_stock_alerte: + literal: '1' + pin__stock: + literal: '1' + pin__stockable_product: + literal: '1' + pin__surface: + literal: '1' + pin__surface_units: + literal: '1' + pin__tobatch: + literal: '1' + pin__tobuy: + literal: '1' + pin__tosell: + literal: '1' + pin__volume: + literal: '1' + pin__volume_units: + literal: '1' + pin__weight: + literal: '1' + pin__weight_units: + literal: '1' + pin__width: + literal: '1' + pin__width_units: + literal: '1' expected_count: 1 - kind: record_written probe: surface=rows_llx_product_price|a row beside the main record holds price_excl_vat @@ -125,6 +277,18 @@ effects: literal: '1' p__price_excl_vat: literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_price_expression: + literal: '1' + pin__multicurrency_price: + literal: '1' + pin__multicurrency_price_ttc: + literal: '1' + pin__multicurrency_tx: + literal: '1' + pin__price_min_ttc: + literal: '1' expected_count: 1 - kind: record_written probe: surface=rows_llx_product_price|a row beside the main record holds vat_rate @@ -139,11 +303,20 @@ effects: literal: '1' p__vat_rate: literal: '1' + pin__fk_multicurrency: + literal: '1' + pin__fk_price_expression: + literal: '1' + pin__multicurrency_price: + literal: '1' + pin__multicurrency_price_ttc: + literal: '1' + pin__multicurrency_tx: + literal: '1' + pin__price_min_ttc: + literal: '1' expected_count: 1 frame: - ignore_fields: - - fk_user_modif - - tms target_fields: [] database: dialect: mysql @@ -157,9 +330,4 @@ frame: views: {} settle_s: 1 settle_attempts: 5 - ignore_columns: - llx_societe: - - fk_user_modif - - socialnetworks - - tms recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml index 9dee225bd5..66bc3dc143 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml @@ -79,10 +79,6 @@ effects: param: town expected_count: 1 frame: - ignore_fields: - - fk_user_modif - - socialnetworks - - tms target_fields: [] versions: read: @@ -91,7 +87,7 @@ frame: sql_connect_args: *id001 sql_password_env: FCG_DOLIBARR_READER_PASSWORD sql_query: SELECT CAST(t.`fk_soc` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`fk_user_author` - AS CHAR) AS `author` FROM `llx_actioncomm` t + AS CHAR) AS `author` FROM `llx_actioncomm` t WHERE CAST(t.`code` AS CHAR) = 'AC_COMPANY_MODIFY' record_field: id target_field: id version_field: ver diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml index 8355de323a..50b08e174d 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml @@ -158,13 +158,12 @@ effects: expected_count: 1 count_new_only: true frame: - ignore_fields: - - fk_user_modif - - socialnetworks - - tms target_fields: - email + - fk_user_modif - phone + - socialnetworks + - tms versions: read: kind: sql @@ -172,7 +171,7 @@ frame: sql_connect_args: *id001 sql_password_env: FCG_DOLIBARR_READER_PASSWORD sql_query: SELECT CAST(t.`fk_soc` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`fk_user_author` - AS CHAR) AS `author` FROM `llx_actioncomm` t + AS CHAR) AS `author` FROM `llx_actioncomm` t WHERE CAST(t.`code` AS CHAR) = 'AC_COMPANY_MODIFY' record_field: id target_field: id version_field: ver diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json index 4ad05e82bf..ac9b1add0e 100644 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json +++ b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json @@ -1,24 +1,39 @@ { - "applied": [], + "applied": [ + "GRANT TRIGGER ON `dolidb`.* TO 'fcg_reader'@'%'" + ], + "read_account": [ + "the read account holds TRIGGER (needed to see triggers; it can also create and drop triggers, so keep its password from the agent)" + ], "dialect": "mysql", "tables": 387, "questions": [ { "number": 1, + "key": "phones", + "text": "These values were stored as other text with the same digits (a phone number written with spaces stored without): create_customer.phone, update_customer.new_phone. Compare them as phone numbers (the same digits, at least 7, and a task value that isn't shaped like a phone number is compared exactly)? Answer `phones: [, ...]` with the ones that are phone numbers; any other is compared exactly.", + "proposal": [ + "create_customer.phone", + "update_customer.new_phone" + ], + "answered": true + }, + { + "number": 2, "key": "bookkeeping", - "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): llx_actioncomm (1 per write), llx_actioncomm_resources (1 per write). Columns that change on their own and are not compared: llx_societe: fk_user_modif, socialnetworks, tms Columns of a new record that differ between demonstrations and are not checked: llx_facture: date_lim_reglement, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref, total_ht, total_ttc, total_tva; llx_product: price_ttc Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", + "text": "Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (llx_societe: the target row): llx_societe: fk_user_modif, tms. Also changed with no task value, and not excused unless you add them under columns_add (a write that changes them stops): llx_societe: socialnetworks. Rows the app adds beside each write, expected exactly that many times: llx_actioncomm (1 per write), llx_actioncomm_resources (1 per write). Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): llx_facture: close_code, close_note, date_lim_reglement, datec, extraparams, fk_currency, import_key, increment, last_main_doc, location_incoterms, model_pdf, module_source, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, note_private, note_public, payment_reference, pos_source, ref, ref_client, ref_ext, tms, total_ht, total_ttc, total_tva; llx_facturedet: batch, description, import_key, label, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref_ext, total_ht, total_ttc, total_tva, vat_src_code; llx_product: accountancy_code_buy, accountancy_code_buy_export, accountancy_code_buy_intra, accountancy_code_sell, accountancy_code_sell_export, accountancy_code_sell_intra, barcode, batch_mask, canvas, customcode, datec, default_vat_code, description, duration, import_key, last_main_doc, model_pdf, note, note_public, partnumber, price_label, price_ttc, ref_ext, tms, url; llx_product_price: date_price, default_vat_code, import_key, multicurrency_code, price_label, price_ttc, tms; llx_societe: accountancy_code_buy, accountancy_code_customer_general, accountancy_code_sell, accountancy_code_supplier_general, ape, barcode, canvas, code_client, code_compta, code_compta_fournisseur, code_fournisseur, datec, default_lang, deposit_percent, fax, fk_currency, fk_prospectlevel, georesultcode, idprof4, idprof5, idprof6, import_key, ip, last_main_doc, location_incoterms, logo, logo_squarred, model_pdf, multicurrency_code, name_alias, note_private, note_public, phone_mobile, prefix_comm, ref_ext, siren, siret, supplier_account, tms, tp_payment_reference, tva_intra, url, webservices_key, webservices_url. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log.", "proposal": "accept", "answered": true }, { - "number": 2, + "number": 3, "key": "recheck_after_s", "text": "How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `.", "proposal": 300, "answered": true }, { - "number": 3, + "number": 4, "key": "locations.create_draft_invoice", "text": "Kind `create_draft_invoice`: `invoice_date` is in several places: [0] llx_facture.datef (date); [1] llx_facture.date_lim_reglement (date) Answer `locations.create_draft_invoice: {: }` to pick one.", "proposal": null, @@ -58,6 +73,9 @@ } ], "logs": [], + "log_candidates": [], + "touched_rows": [], + "unexcused": {}, "refused": [], "unresolved": {}, "emitted": 64, @@ -95,6 +113,9 @@ } ], "logs": [], + "log_candidates": [], + "touched_rows": [], + "unexcused": {}, "refused": [], "unresolved": {}, "emitted": 4, @@ -119,6 +140,9 @@ }, "side_rows": [], "logs": [], + "log_candidates": [], + "touched_rows": [], + "unexcused": {}, "refused": [], "unresolved": {}, "emitted": 10, @@ -145,7 +169,9 @@ "record_column": "fk_soc", "author_column": "fk_user_author", "actor_param": "bot_user_id", - "where": {} + "where": { + "code": "AC_COMPANY_MODIFY" + } }, "placed_from_another_kind": [], "demonstrations": 2, @@ -165,6 +191,9 @@ } ], "logs": [], + "log_candidates": [], + "touched_rows": [], + "unexcused": {}, "refused": [], "unresolved": {}, "emitted": 15, diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md index b176960544..247d519f13 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md @@ -1,10 +1,14 @@ # Questions for a person -1. Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): call_user (1 per write), note (1 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change): action_history_record, auth_log_record, note, note_user Columns that change on their own and are not compared: account: stream_updated_at; contact: stream_updated_at; lead: modified_at, modified_by_id, stream_updated_at Columns of a new record that differ between demonstrations and are not checked: call: parent_type, uid; opportunity: last_stage, probability Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. +1. These values were stored as other text with the same digits (a phone number written with spaces stored without): create_contact.phone. Compare them as phone numbers (the same digits, at least 7, and a task value that isn't shaped like a phone number is compared exactly)? Answer `phones: [, ...]` with the ones that are phone numbers; any other is compared exactly. + - Answer key: `phones`. Proposed: `['create_contact.phone']`. + - Answered: `["create_contact.phone"]` + +2. Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (account: the account row the task's account, account_city name; lead: the lead row the task's lead_company name, the target row): account: stream_updated_at; lead: modified_at, modified_by_id, stream_updated_at. Changed on rows the generator can't identify, so a write that changes them stops: contact: stream_updated_at. Rows the app adds beside each write, expected exactly that many times: action_history_record (1 per write), auth_log_record (1 per write), call_user (1 per write), note (1 per write). Tables that gained a different number of rows in different demonstrations; they are not logs unless you list them under logs_add (rows may then be added, existing rows must not change): note, note_user. Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): account_contact: role; call: created_at, modified_at, modified_by_id, parent_type, uid; contact: address_city, address_country, address_postal_code, address_state, address_street, assigned_user_id, campaign_id, created_at, description, middle_name, modified_at, modified_by_id, salutation_name, stream_updated_at; opportunity: assigned_user_id, campaign_id, contact_id, created_at, description, last_stage, lead_source, modified_at, modified_by_id, probability, stream_updated_at. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log. - Answer key: `bookkeeping`. Proposed: `accept`. - - Answered: `"accept"` + - Answered: `{"logs_add": ["action_history_record", "auth_log_record"]}` -2. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. +3. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. - Answer key: `recheck_after_s`. Proposed: `300`. - Answered: `300` diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml index 7efebeb6dc..21499e1086 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml @@ -1,2 +1,4 @@ -bookkeeping: accept +phones: [create_contact.phone] +bookkeeping: + logs_add: [action_history_record, auth_log_record] recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml index 0a79c06781..0c52689923 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml @@ -26,12 +26,19 @@ verifier: CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, - CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM - `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, - (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) - = CAST(t.`account_id` AS BINARY)) AS `p__account_city`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__deleted`, CASE WHEN t.`do_not_call` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__do_not_call` FROM `contact` t + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, (SELECT CASE WHEN (SELECT COUNT(*) FROM + `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) + AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM + `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, + 0) = 0) AS `p__account`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` + AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, + 0) = 0) = 1 THEN CAST(a1.`billing_address_city` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` + AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, + CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, CASE + WHEN t.`do_not_call` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__do_not_call` + FROM `contact` t rows_account_contact: kind: sql sql_driver: pymysql @@ -39,18 +46,33 @@ verifier: sql_password_env: ESPOCRM_DB_RO_PASSWORD sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`role` AS CHAR) AS `role`, CAST(t.`is_inactive` AS CHAR) AS `is_inactive`, - CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` - a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` a1 WHERE a1.`id` - = t.`contact_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` AS CHAR) - FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` - a1 WHERE a1.`id` = t.`contact_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` AS CHAR) - FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` - AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__last_name`, (SELECT CAST(a1.`name` - AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS - `p__account`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, - CASE WHEN t.`is_inactive` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__is_inactive`, - (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) - = CAST(t.`account_id` AS BINARY)) AS `p__account_city` FROM `account_contact` t + CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` + u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND + CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM + `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, + 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS + `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` + AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, + 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` + AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` + a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, + (SELECT CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` + = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT + COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM + `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, + 0) = 0) AS `p__account`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' + END AS `pin__deleted`, CASE WHEN t.`is_inactive` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' + END AS `pin__is_inactive`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` + AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, + 0) = 0) = 1 THEN CAST(a1.`billing_address_city` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` + AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city` + FROM `account_contact` t rows_entity_email_address: kind: sql sql_driver: pymysql @@ -58,17 +80,27 @@ verifier: sql_password_env: ESPOCRM_DB_RO_PASSWORD sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` - AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` - AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM - `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` - AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM - `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` - AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` - AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` - AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS - BINARY)) AS `p__email`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' - END AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__primary` FROM `entity_email_address` t + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT + COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM + `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, + 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS + `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` + AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, + 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` + AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` + a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, (SELECT + CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` + = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT + COUNT(*) FROM `email_address` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM + `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS BINARY) AND COALESCE(a1.`deleted`, + 0) = 0) AS `p__email`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `pin__primary` FROM `entity_email_address` t rows_entity_phone_number: kind: sql sql_driver: pymysql @@ -76,17 +108,47 @@ verifier: sql_password_env: ESPOCRM_DB_RO_PASSWORD sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`phone_number_id` AS CHAR) AS `phone_number_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` - AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` - AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM - `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` - AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM - `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` - AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` - AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` - AS CHAR) FROM `phone_number` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`phone_number_id` AS BINARY)) - AS `p__phone`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, - CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__primary` FROM - `entity_phone_number` t + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT + COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM + `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, + 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS + `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` + AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, + 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` + AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` + a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, (SELECT + CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` + = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT + COUNT(*) FROM `phone_number` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM + `phone_number` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`phone_number_id` AS BINARY) AND COALESCE(a1.`deleted`, + 0) = 0) AS `p__phone`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `pin__primary` FROM `entity_phone_number` t + raw_account: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`type` AS CHAR) AS `type`, + CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`sic_code` AS CHAR) AS `sic_code`, CAST(t.`billing_address_street` + AS CHAR) AS `billing_address_street`, CAST(t.`billing_address_city` AS CHAR) AS `billing_address_city`, + CAST(t.`billing_address_state` AS CHAR) AS `billing_address_state`, CAST(t.`billing_address_country` + AS CHAR) AS `billing_address_country`, CAST(t.`billing_address_postal_code` AS CHAR) AS `billing_address_postal_code`, + CAST(t.`shipping_address_street` AS CHAR) AS `shipping_address_street`, CAST(t.`shipping_address_city` + AS CHAR) AS `shipping_address_city`, CAST(t.`shipping_address_state` AS CHAR) AS `shipping_address_state`, + CAST(t.`shipping_address_country` AS CHAR) AS `shipping_address_country`, CAST(t.`shipping_address_postal_code` + AS CHAR) AS `shipping_address_postal_code`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`is_locked` + AS CHAR) AS `is_locked`, CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS + CHAR) AS `modified_at`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`campaign_id` + AS CHAR) AS `campaign_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` + AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` + AS CHAR) AS `version_number` FROM `account` t raw_email_address: kind: sql sql_driver: pymysql @@ -240,6 +302,14 @@ effects: literal: '1' expected_count: 1 count_new_only: true +- kind: record_written + probe: surface=raw_account|the account row the task's account, account_city name, changed by the app + match: + name: + param: account + billing_address_city: + param: account_city + expected_count: 1 - kind: record_written probe: surface=raw_email_address|the new email_address row holds email match: @@ -278,11 +348,8 @@ effects: expected_count: 1 count_new_only: true frame: - ignore_fields: - - modified_at - - modified_by_id + target_fields: - stream_updated_at - target_fields: [] database: dialect: mysql read: @@ -297,6 +364,7 @@ frame: - table: entity_email_address - table: entity_phone_number views: + account: raw_account email_address: raw_email_address phone_number: raw_phone_number note: raw_note @@ -329,11 +397,4 @@ frame: - related_type - super_parent_id - target_type - ignore_columns: - account: - - stream_updated_at - lead: - - modified_at - - modified_by_id - - stream_updated_at recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml index 5ac58e6a52..3776794630 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml @@ -26,10 +26,19 @@ verifier: CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, - CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM - `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, - (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) - = CAST(t.`account_id` AS BINARY)) AS `p__account_city` FROM `contact` t + CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, (SELECT CASE WHEN (SELECT COUNT(*) FROM + `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) + AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM + `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, + 0) = 0) AS `p__account`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` + AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, + 0) = 0) = 1 THEN CAST(a1.`billing_address_city` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` + AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, + CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, CASE + WHEN t.`do_not_call` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__do_not_call` + FROM `contact` t rows_account_contact: kind: sql sql_driver: pymysql @@ -37,16 +46,33 @@ verifier: sql_password_env: ESPOCRM_DB_RO_PASSWORD sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`role` AS CHAR) AS `role`, CAST(t.`is_inactive` AS CHAR) AS `is_inactive`, - CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` - a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` a1 WHERE a1.`id` - = t.`contact_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` AS CHAR) - FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM `contact` - a1 WHERE a1.`id` = t.`contact_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` AS CHAR) - FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` - AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`contact_id`) AS `main__last_name`, (SELECT CAST(a1.`name` - AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS - `p__account`, (SELECT CAST(a1.`billing_address_city` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` - AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account_city` FROM `account_contact` t + CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` + u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND + CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM + `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, + 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS + `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` + AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, + 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` + AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` + a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, + (SELECT CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` + = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT + COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM + `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, + 0) = 0) AS `p__account`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' + END AS `pin__deleted`, CASE WHEN t.`is_inactive` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' + END AS `pin__is_inactive`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` + AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, + 0) = 0) = 1 THEN CAST(a1.`billing_address_city` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` + AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city` + FROM `account_contact` t rows_entity_email_address: kind: sql sql_driver: pymysql @@ -54,15 +80,27 @@ verifier: sql_password_env: ESPOCRM_DB_RO_PASSWORD sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` - AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` - AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM - `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` - AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM - `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` - AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` - AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` - AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS - BINARY)) AS `p__email` FROM `entity_email_address` t + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT + COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM + `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, + 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS + `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` + AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, + 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` + AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` + a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, (SELECT + CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` + = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT + COUNT(*) FROM `email_address` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM + `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS BINARY) AND COALESCE(a1.`deleted`, + 0) = 0) AS `p__email`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `pin__primary` FROM `entity_email_address` t rows_entity_phone_number: kind: sql sql_driver: pymysql @@ -70,15 +108,27 @@ verifier: sql_password_env: ESPOCRM_DB_RO_PASSWORD sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`phone_number_id` AS CHAR) AS `phone_number_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` - AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CAST(a2.`name` - AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM - `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` - AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM - `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__account_city`, (SELECT CAST(a1.`first_name` - AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` - AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` - AS CHAR) FROM `phone_number` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`phone_number_id` AS BINARY)) - AS `p__phone` FROM `entity_phone_number` t + AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT + COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM + `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, + 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS + `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` + AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, + 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` + AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` + a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, (SELECT + CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` + = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT + COUNT(*) FROM `phone_number` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM + `phone_number` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`phone_number_id` AS BINARY) AND COALESCE(a1.`deleted`, + 0) = 0) AS `p__phone`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `pin__primary` FROM `entity_phone_number` t effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -91,6 +141,10 @@ effects: param: first_name last_name: param: last_name + pin__deleted: + literal: '1' + pin__do_not_call: + literal: '1' expected_count: 1 - kind: record_written probe: surface=rows_account_contact|a row beside the main record holds account @@ -105,6 +159,10 @@ effects: param: last_name p__account: param: account + pin__deleted: + literal: '1' + pin__is_inactive: + literal: '1' expected_count: 1 - kind: record_written probe: surface=rows_account_contact|a row beside the main record holds account_city @@ -119,6 +177,10 @@ effects: param: last_name p__account_city: param: account_city + pin__deleted: + literal: '1' + pin__is_inactive: + literal: '1' expected_count: 1 - kind: record_written probe: surface=rows_entity_email_address|a row beside the main record holds email @@ -134,6 +196,12 @@ effects: p__email: param: email kind: email + pin__deleted: + literal: '1' + entity_type: + literal: Contact + pin__primary: + literal: '1' expected_count: 1 - kind: record_written probe: surface=rows_entity_phone_number|a row beside the main record holds phone @@ -149,6 +217,12 @@ effects: p__phone: param: phone kind: phone + pin__deleted: + literal: '1' + entity_type: + literal: Contact + pin__primary: + literal: '1' expected_count: 1 - kind: record_written probe: surface=rows_account_contact|a row beside the main record holds title @@ -163,12 +237,12 @@ effects: param: last_name role: param: title + pin__deleted: + literal: '1' + pin__is_inactive: + literal: '1' expected_count: 1 frame: - ignore_fields: - - modified_at - - modified_by_id - - stream_updated_at target_fields: [] database: dialect: mysql @@ -186,11 +260,4 @@ frame: views: {} settle_s: 1 settle_attempts: 5 - ignore_columns: - account: - - stream_updated_at - lead: - - modified_at - - modified_by_id - - stream_updated_at recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml index 02c8105314..03417572ff 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml @@ -26,20 +26,46 @@ verifier: CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` AS CHAR) AS - `version_number`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) - = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` AS CHAR) - FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account_city`, - CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`amount` = CAST(%(v0)s AS - DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__amount`, CASE WHEN t.`close_date` = CAST(%(v1)s AS - DATE) THEN '1' ELSE '0' END AS `p__close_date`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__deleted`, CASE WHEN t.`version_number` = (SELECT s.`version_number` - FROM `account` s WHERE s.`id` = t.`account_id`) THEN '1' ELSE '0' END AS `pin__version_number` - FROM `opportunity` t + `version_number`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` + AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, + 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = + CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account`, (SELECT CASE + WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`billing_address_city` + AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) + AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP + '^-?[0-9]+([.][0-9]+)?$' AND t.`amount` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `p__amount`, CASE WHEN t.`close_date` = CAST(%(v1)s AS DATE) THEN '1' ELSE '0' END AS `p__close_date`, + CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, CASE + WHEN t.`version_number` = (SELECT s.`version_number` FROM `account` s WHERE s.`id` = t.`account_id`) + THEN '1' ELSE '0' END AS `pin__version_number` FROM `opportunity` t sql_query_params: v0: param: amount v1: param: close_date + raw_account: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`type` AS CHAR) AS `type`, + CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`sic_code` AS CHAR) AS `sic_code`, CAST(t.`billing_address_street` + AS CHAR) AS `billing_address_street`, CAST(t.`billing_address_city` AS CHAR) AS `billing_address_city`, + CAST(t.`billing_address_state` AS CHAR) AS `billing_address_state`, CAST(t.`billing_address_country` + AS CHAR) AS `billing_address_country`, CAST(t.`billing_address_postal_code` AS CHAR) AS `billing_address_postal_code`, + CAST(t.`shipping_address_street` AS CHAR) AS `shipping_address_street`, CAST(t.`shipping_address_city` + AS CHAR) AS `shipping_address_city`, CAST(t.`shipping_address_state` AS CHAR) AS `shipping_address_state`, + CAST(t.`shipping_address_country` AS CHAR) AS `shipping_address_country`, CAST(t.`shipping_address_postal_code` + AS CHAR) AS `shipping_address_postal_code`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`is_locked` + AS CHAR) AS `is_locked`, CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS + CHAR) AS `modified_at`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`campaign_id` + AS CHAR) AS `campaign_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` + AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` + AS CHAR) AS `version_number` FROM `account` t raw_note: kind: sql sql_driver: pymysql @@ -81,6 +107,14 @@ effects: literal: '1' expected_count: 1 count_new_only: true +- kind: record_written + probe: surface=raw_account|the account row the task's account, account_city name, changed by the app + match: + name: + param: account + billing_address_city: + param: account_city + expected_count: 1 - kind: record_written probe: surface=raw_note|1 row(s) the app adds to note match: @@ -103,11 +137,8 @@ effects: expected_count: 1 count_new_only: true frame: - ignore_fields: - - modified_at - - modified_by_id + target_fields: - stream_updated_at - target_fields: [] database: dialect: mysql read: @@ -119,6 +150,7 @@ frame: - table: auth_log_record - table: opportunity views: + account: raw_account note: raw_note settle_s: 1 settle_attempts: 5 @@ -136,13 +168,4 @@ frame: - related_type - super_parent_id - target_type - ignore_columns: - account: - - stream_updated_at - lead: - - modified_at - - modified_by_id - - stream_updated_at - contact: - - stream_updated_at recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml index 1942899ebc..f61d477b2a 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml @@ -26,12 +26,20 @@ verifier: CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` AS CHAR) AS - `version_number`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) - = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` AS CHAR) - FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account_city`, - CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`amount` = CAST(%(v0)s AS - DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__amount`, CASE WHEN t.`close_date` = CAST(%(v1)s AS - DATE) THEN '1' ELSE '0' END AS `p__close_date` FROM `opportunity` t + `version_number`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` + AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, + 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = + CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account`, (SELECT CASE + WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`billing_address_city` + AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) + AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP + '^-?[0-9]+([.][0-9]+)?$' AND t.`amount` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `p__amount`, CASE WHEN t.`close_date` = CAST(%(v1)s AS DATE) THEN '1' ELSE '0' END AS `p__close_date`, + CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted` FROM + `opportunity` t sql_query_params: v0: param: amount @@ -55,12 +63,10 @@ effects: param: name stage: param: stage + pin__deleted: + literal: '1' expected_count: 1 frame: - ignore_fields: - - modified_at - - modified_by_id - - stream_updated_at target_fields: [] database: dialect: mysql @@ -75,13 +81,4 @@ frame: views: {} settle_s: 1 settle_attempts: 5 - ignore_columns: - account: - - stream_updated_at - lead: - - modified_at - - modified_by_id - - stream_updated_at - contact: - - stream_updated_at recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml index b17aa26683..c6a4812965 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml @@ -23,14 +23,20 @@ verifier: CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` AS CHAR) AS `parent_type`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` - AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` - AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` - AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS - `p__account_city`, CASE WHEN t.`date_start` = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS - `p__date_start`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (TIMESTAMPDIFF(SECOND, - t.`date_start`, t.`date_end`) / 60) = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `p__duration_minutes`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__deleted` FROM `call` t + AS CHAR) AS `assigned_user_id`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` + AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, + 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = + CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account`, (SELECT CASE + WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`billing_address_city` + AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) + AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, CASE WHEN t.`date_start` = CAST(%(v0)s + AS DATETIME) THEN '1' ELSE '0' END AS `p__date_start`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND (TIMESTAMPDIFF(SECOND, t.`date_start`, t.`date_end`) / 60) = CAST(%(v1)s AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `p__duration_minutes`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__deleted` FROM `call` t sql_query_params: v0: &id002 param: date_start @@ -43,24 +49,57 @@ verifier: sql_password_env: ESPOCRM_DB_RO_PASSWORD sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`call_id` AS CHAR) AS `call_id`, CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`deleted` AS CHAR) AS `deleted`, - (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` - AS BINARY)) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` - AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM - `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account_city`, CASE WHEN (SELECT a1.`date_start` - FROM `call` a1 WHERE a1.`id` = t.`call_id`) = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS - `main__date_start`, (SELECT CAST(a1.`description` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) - AS `main__description`, (SELECT CAST(a1.`direction` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) - AS `main__direction`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT - (TIMESTAMPDIFF(SECOND, a1.`date_start`, a1.`date_end`) / 60) FROM `call` a1 WHERE a1.`id` = t.`call_id`) - = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__duration_minutes`, (SELECT CAST(a1.`status` - AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__status`, (SELECT CAST(a1.`name` - AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__subject`, (SELECT CONCAT(CAST(a1.`first_name` - AS CHAR), ' ', CAST(a1.`last_name` AS CHAR)) FROM `contact` a1 WHERE CAST(a1.`id` AS BINARY) = - CAST(t.`contact_id` AS BINARY)) AS `p__contact`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__deleted` FROM `call_contact` t + (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) + AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS + CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, + 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = + CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `call` a1 WHERE a1.`id` + = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account`, (SELECT (SELECT CASE WHEN + (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`billing_address_city` + AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) + AND COALESCE(a2.`deleted`, 0) = 0) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__account_city`, CASE WHEN (SELECT a1.`date_start` FROM `call` a1 WHERE a1.`id` + = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' + END AS `main__date_start`, (SELECT CAST(a1.`description` AS CHAR) FROM `call` a1 WHERE a1.`id` + = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__description`, (SELECT CAST(a1.`direction` + AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__direction`, + CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT (TIMESTAMPDIFF(SECOND, + a1.`date_start`, a1.`date_end`) / 60) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, + 0) = 0) = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__duration_minutes`, (SELECT + CAST(a1.`status` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__status`, (SELECT CAST(a1.`name` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` + AND COALESCE(a1.`deleted`, 0) = 0) AS `main__subject`, (SELECT CASE WHEN (SELECT COUNT(*) FROM + `contact` u1 WHERE CAST(CONCAT(CAST(u1.`first_name` AS CHAR), ' ', CAST(u1.`last_name` AS CHAR)) + AS BINARY) = CAST(CONCAT(CAST(a1.`first_name` AS CHAR), ' ', CAST(a1.`last_name` AS CHAR)) AS + BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CONCAT(CAST(a1.`first_name` AS CHAR), ' ', + CAST(a1.`last_name` AS CHAR)) END FROM `contact` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`contact_id` + AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__contact`, CASE WHEN t.`deleted` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted` FROM `call_contact` t sql_query_params: v0: *id002 v1: *id003 + raw_account: + kind: sql + sql_driver: pymysql + sql_connect_args: *id001 + sql_password_env: ESPOCRM_DB_RO_PASSWORD + sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` + AS CHAR) AS `deleted`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`type` AS CHAR) AS `type`, + CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`sic_code` AS CHAR) AS `sic_code`, CAST(t.`billing_address_street` + AS CHAR) AS `billing_address_street`, CAST(t.`billing_address_city` AS CHAR) AS `billing_address_city`, + CAST(t.`billing_address_state` AS CHAR) AS `billing_address_state`, CAST(t.`billing_address_country` + AS CHAR) AS `billing_address_country`, CAST(t.`billing_address_postal_code` AS CHAR) AS `billing_address_postal_code`, + CAST(t.`shipping_address_street` AS CHAR) AS `shipping_address_street`, CAST(t.`shipping_address_city` + AS CHAR) AS `shipping_address_city`, CAST(t.`shipping_address_state` AS CHAR) AS `shipping_address_state`, + CAST(t.`shipping_address_country` AS CHAR) AS `shipping_address_country`, CAST(t.`shipping_address_postal_code` + AS CHAR) AS `shipping_address_postal_code`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`is_locked` + AS CHAR) AS `is_locked`, CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS + CHAR) AS `modified_at`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`campaign_id` + AS CHAR) AS `campaign_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` + AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` + AS CHAR) AS `version_number` FROM `account` t raw_call_user: kind: sql sql_driver: pymysql @@ -126,6 +165,14 @@ effects: literal: None expected_count: 1 count_new_only: true +- kind: record_written + probe: surface=raw_account|the account row the task's account, account_city name, changed by the app + match: + name: + param: account + billing_address_city: + param: account_city + expected_count: 1 - kind: record_written probe: surface=raw_call_user|1 row(s) the app adds to call_user match: @@ -138,10 +185,8 @@ effects: expected_count: 1 count_new_only: true frame: - ignore_fields: - - modified_at - - modified_by_id - target_fields: [] + target_fields: + - stream_updated_at database: dialect: mysql read: @@ -151,11 +196,10 @@ frame: allow_tables: - table: action_history_record - table: auth_log_record - - table: note - - table: note_user - table: call - table: call_contact views: + account: raw_account call_user: raw_call_user settle_s: 1 settle_attempts: 5 @@ -163,13 +207,4 @@ frame: call_user: - call_id - id - ignore_columns: - account: - - stream_updated_at - lead: - - modified_at - - modified_by_id - - stream_updated_at - contact: - - stream_updated_at recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml index 6f722304fc..5fa761951a 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml @@ -23,13 +23,20 @@ verifier: CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` AS CHAR) AS `parent_type`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` - AS CHAR) AS `assigned_user_id`, (SELECT CAST(a1.`name` AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` - AS BINARY) = CAST(t.`account_id` AS BINARY)) AS `p__account`, (SELECT CAST(a1.`billing_address_city` - AS CHAR) FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY)) AS - `p__account_city`, CASE WHEN t.`date_start` = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS - `p__date_start`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (TIMESTAMPDIFF(SECOND, - t.`date_start`, t.`date_end`) / 60) = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `p__duration_minutes` FROM `call` t + AS CHAR) AS `assigned_user_id`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` + AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` + AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, + 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = + CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account`, (SELECT CASE + WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`billing_address_city` + AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) + AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, CASE WHEN t.`date_start` = CAST(%(v0)s + AS DATETIME) THEN '1' ELSE '0' END AS `p__date_start`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' + AND (TIMESTAMPDIFF(SECOND, t.`date_start`, t.`date_end`) / 60) = CAST(%(v1)s AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `p__duration_minutes`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) + THEN '1' ELSE '0' END AS `pin__deleted` FROM `call` t sql_query_params: v0: &id002 param: date_start @@ -42,20 +49,34 @@ verifier: sql_password_env: ESPOCRM_DB_RO_PASSWORD sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`call_id` AS CHAR) AS `call_id`, CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`deleted` AS CHAR) AS `deleted`, - (SELECT (SELECT CAST(a2.`name` AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` - AS BINARY)) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account`, (SELECT (SELECT CAST(a2.`billing_address_city` - AS CHAR) FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY)) FROM - `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__account_city`, CASE WHEN (SELECT a1.`date_start` - FROM `call` a1 WHERE a1.`id` = t.`call_id`) = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' END AS - `main__date_start`, (SELECT CAST(a1.`description` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) - AS `main__description`, (SELECT CAST(a1.`direction` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) - AS `main__direction`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT - (TIMESTAMPDIFF(SECOND, a1.`date_start`, a1.`date_end`) / 60) FROM `call` a1 WHERE a1.`id` = t.`call_id`) - = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__duration_minutes`, (SELECT CAST(a1.`status` - AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__status`, (SELECT CAST(a1.`name` - AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id`) AS `main__subject`, (SELECT CONCAT(CAST(a1.`first_name` - AS CHAR), ' ', CAST(a1.`last_name` AS CHAR)) FROM `contact` a1 WHERE CAST(a1.`id` AS BINARY) = - CAST(t.`contact_id` AS BINARY)) AS `p__contact` FROM `call_contact` t + (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) + AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS + CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, + 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = + CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `call` a1 WHERE a1.`id` + = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account`, (SELECT (SELECT CASE WHEN + (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` + AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` + AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`billing_address_city` + AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) + AND COALESCE(a2.`deleted`, 0) = 0) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__account_city`, CASE WHEN (SELECT a1.`date_start` FROM `call` a1 WHERE a1.`id` + = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' + END AS `main__date_start`, (SELECT CAST(a1.`description` AS CHAR) FROM `call` a1 WHERE a1.`id` + = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__description`, (SELECT CAST(a1.`direction` + AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__direction`, + CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT (TIMESTAMPDIFF(SECOND, + a1.`date_start`, a1.`date_end`) / 60) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, + 0) = 0) = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__duration_minutes`, (SELECT + CAST(a1.`status` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__status`, (SELECT CAST(a1.`name` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` + AND COALESCE(a1.`deleted`, 0) = 0) AS `main__subject`, (SELECT CASE WHEN (SELECT COUNT(*) FROM + `contact` u1 WHERE CAST(CONCAT(CAST(u1.`first_name` AS CHAR), ' ', CAST(u1.`last_name` AS CHAR)) + AS BINARY) = CAST(CONCAT(CAST(a1.`first_name` AS CHAR), ' ', CAST(a1.`last_name` AS CHAR)) AS + BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CONCAT(CAST(a1.`first_name` AS CHAR), ' ', + CAST(a1.`last_name` AS CHAR)) END FROM `contact` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`contact_id` + AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__contact`, CASE WHEN t.`deleted` = CAST('0' + AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted` FROM `call_contact` t sql_query_params: v0: *id002 v1: *id003 @@ -79,6 +100,10 @@ effects: param: status name: param: subject + pin__deleted: + literal: '1' + parent_type: + literal: Account expected_count: 1 - kind: record_written probe: surface=rows_call_contact|a row beside the main record holds contact @@ -101,11 +126,12 @@ effects: param: subject p__contact: param: contact + pin__deleted: + literal: '1' + status: + literal: None expected_count: 1 frame: - ignore_fields: - - modified_at - - modified_by_id target_fields: [] database: dialect: mysql @@ -116,20 +142,9 @@ frame: allow_tables: - table: action_history_record - table: auth_log_record - - table: note - - table: note_user - table: call - table: call_contact views: {} settle_s: 1 settle_attempts: 5 - ignore_columns: - account: - - stream_updated_at - lead: - - modified_at - - modified_by_id - - stream_updated_at - contact: - - stream_updated_at recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml index 0d63d283c1..a9b5932b78 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml @@ -41,11 +41,16 @@ verifier: sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT CAST(a1.`account_name` - AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__company`, (SELECT CAST(a1.`first_name` - AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` - AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` - AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS - BINARY)) AS `p__value` FROM `entity_email_address` t + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__company`, + (SELECT CAST(a1.`first_name` AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `lead` a1 WHERE a1.`id` + = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT + COUNT(*) FROM `email_address` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM + `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS BINARY) AND COALESCE(a1.`deleted`, + 0) = 0) AS `p__value`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `pin__primary` FROM `entity_email_address` t effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -69,12 +74,14 @@ effects: p__value: param: value kind: email + pin__deleted: + literal: '1' + entity_type: + literal: Lead + pin__primary: + literal: '1' expected_count: 1 frame: - ignore_fields: - - modified_at - - modified_by_id - - stream_updated_at target_fields: [] versions: read: @@ -83,7 +90,8 @@ frame: sql_connect_args: *id001 sql_password_env: ESPOCRM_DB_RO_PASSWORD sql_query: SELECT CAST(t.`target_id` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`user_id` - AS CHAR) AS `author` FROM `action_history_record` t WHERE CAST(t.`target_type` AS CHAR) = 'Lead' + AS CHAR) AS `author` FROM `action_history_record` t WHERE CAST(t.`action` AS CHAR) = 'update' + AND CAST(t.`target_type` AS CHAR) = 'Lead' record_field: id target_field: id version_field: ver @@ -99,15 +107,9 @@ frame: allow_tables: - table: action_history_record - table: auth_log_record - - table: note - table: entity_email_address - table: lead views: {} settle_s: 1 settle_attempts: 5 - ignore_columns: - account: - - stream_updated_at - contact: - - stream_updated_at recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml index 8063d931de..d2d679b8dd 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml @@ -41,13 +41,16 @@ verifier: sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT CAST(a1.`account_name` - AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__company`, (SELECT CAST(a1.`first_name` - AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__first_name`, (SELECT CAST(a1.`last_name` - AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id`) AS `main__last_name`, (SELECT CAST(a1.`name` - AS CHAR) FROM `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS - BINARY)) AS `p__value`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' - END AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__primary` FROM `entity_email_address` t + AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__company`, + (SELECT CAST(a1.`first_name` AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, + 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `lead` a1 WHERE a1.`id` + = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT + COUNT(*) FROM `email_address` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` + AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM + `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS BINARY) AND COALESCE(a1.`deleted`, + 0) = 0) AS `p__value`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END + AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS + `pin__primary` FROM `entity_email_address` t raw_email_address: kind: sql sql_driver: pymysql @@ -106,11 +109,10 @@ effects: expected_count: 1 count_new_only: true frame: - ignore_fields: + target_fields: - modified_at - modified_by_id - stream_updated_at - target_fields: [] versions: read: kind: sql @@ -118,7 +120,8 @@ frame: sql_connect_args: *id001 sql_password_env: ESPOCRM_DB_RO_PASSWORD sql_query: SELECT CAST(t.`target_id` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`user_id` - AS CHAR) AS `author` FROM `action_history_record` t WHERE CAST(t.`target_type` AS CHAR) = 'Lead' + AS CHAR) AS `author` FROM `action_history_record` t WHERE CAST(t.`action` AS CHAR) = 'update' + AND CAST(t.`target_type` AS CHAR) = 'Lead' record_field: id target_field: id version_field: ver @@ -134,7 +137,6 @@ frame: allow_tables: - table: action_history_record - table: auth_log_record - - table: note - table: entity_email_address mode: skip reason: read in full, every column, by this contract's own surface @@ -152,9 +154,4 @@ frame: - invalid - lower - opt_out - ignore_columns: - account: - - stream_updated_at - contact: - - stream_updated_at recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/report.json b/packages/flow/benchmark/generate_contracts/results/espocrm/report.json index 173dc0a68b..880ea3e522 100644 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/report.json +++ b/packages/flow/benchmark/generate_contracts/results/espocrm/report.json @@ -1,17 +1,29 @@ { "applied": [], + "read_account": [ + "the read account holds TRIGGER (needed to see triggers; it can also create and drop triggers, so keep its password from the agent)" + ], "dialect": "mysql", "tables": 141, "questions": [ { "number": 1, + "key": "phones", + "text": "These values were stored as other text with the same digits (a phone number written with spaces stored without): create_contact.phone. Compare them as phone numbers (the same digits, at least 7, and a task value that isn't shaped like a phone number is compared exactly)? Answer `phones: [, ...]` with the ones that are phone numbers; any other is compared exactly.", + "proposal": [ + "create_contact.phone" + ], + "answered": true + }, + { + "number": 2, "key": "bookkeeping", - "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): call_user (1 per write), note (1 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change): action_history_record, auth_log_record, note, note_user Columns that change on their own and are not compared: account: stream_updated_at; contact: stream_updated_at; lead: modified_at, modified_by_id, stream_updated_at Columns of a new record that differ between demonstrations and are not checked: call: parent_type, uid; opportunity: last_stage, probability Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", + "text": "Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (account: the account row the task's account, account_city name; lead: the lead row the task's lead_company name, the target row): account: stream_updated_at; lead: modified_at, modified_by_id, stream_updated_at. Changed on rows the generator can't identify, so a write that changes them stops: contact: stream_updated_at. Rows the app adds beside each write, expected exactly that many times: action_history_record (1 per write), auth_log_record (1 per write), call_user (1 per write), note (1 per write). Tables that gained a different number of rows in different demonstrations; they are not logs unless you list them under logs_add (rows may then be added, existing rows must not change): note, note_user. Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): account_contact: role; call: created_at, modified_at, modified_by_id, parent_type, uid; contact: address_city, address_country, address_postal_code, address_state, address_street, assigned_user_id, campaign_id, created_at, description, middle_name, modified_at, modified_by_id, salutation_name, stream_updated_at; opportunity: assigned_user_id, campaign_id, contact_id, created_at, description, last_stage, lead_source, modified_at, modified_by_id, probability, stream_updated_at. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log.", "proposal": "accept", "answered": true }, { - "number": 2, + "number": 3, "key": "recheck_after_s", "text": "How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `.", "proposal": 300, @@ -55,6 +67,26 @@ "action_history_record", "auth_log_record" ], + "log_candidates": [], + "touched_rows": [ + { + "table": "account", + "selector": { + "name": { + "param": "account" + }, + "billing_address_city": { + "param": "account_city" + } + }, + "columns": { + "stream_updated_at": "timestamp" + }, + "row": "the account row the task's account, account_city name", + "by_key": false + } + ], + "unexcused": {}, "refused": [], "unresolved": {}, "emitted": 64, @@ -90,6 +122,26 @@ "action_history_record", "auth_log_record" ], + "log_candidates": [], + "touched_rows": [ + { + "table": "account", + "selector": { + "name": { + "param": "account" + }, + "billing_address_city": { + "param": "account_city" + } + }, + "columns": { + "stream_updated_at": "timestamp" + }, + "row": "the account row the task's account, account_city name", + "by_key": false + } + ], + "unexcused": {}, "refused": [], "unresolved": {}, "emitted": 64, @@ -111,7 +163,8 @@ "author_column": "user_id", "actor_param": "bot_user_id", "where": { - "target_type": "Lead" + "target_type": "Lead", + "action": "update" } }, "placed_from_another_kind": [], @@ -126,9 +179,13 @@ "side_rows": [], "logs": [ "action_history_record", - "auth_log_record", + "auth_log_record" + ], + "log_candidates": [ "note" ], + "touched_rows": [], + "unexcused": {}, "refused": [], "unresolved": {}, "emitted": 43, @@ -170,10 +227,48 @@ ], "logs": [ "action_history_record", - "auth_log_record", + "auth_log_record" + ], + "log_candidates": [ "note", "note_user" ], + "touched_rows": [ + { + "table": "account", + "selector": { + "name": { + "param": "account" + }, + "billing_address_city": { + "param": "account_city" + } + }, + "columns": { + "stream_updated_at": "timestamp" + }, + "row": "the account row the task's account, account_city name", + "by_key": false + }, + { + "table": "lead", + "selector": { + "account_name": { + "param": "lead_company" + } + }, + "columns": { + "stream_updated_at": "timestamp" + }, + "row": "the lead row the task's lead_company name", + "by_key": false + } + ], + "unexcused": { + "contact": [ + "stream_updated_at" + ] + }, "refused": [], "unresolved": {}, "emitted": 64, diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md index cef010825f..4eab26f265 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md +++ b/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md @@ -1,8 +1,12 @@ # Questions for a person -1. Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): custom_values (2 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change; on PostgreSQL they are not read at all): journal_details Columns that change on their own and are not compared: issues: lock_version, updated_on; user_preferences: others Columns of a new record that differ between demonstrations and are not checked: time_entries: tmonth, tweek, tyear Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove. +1. Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (issues: the target row): issues: lock_version, updated_on. Also changed with no task value, and not excused unless you add them under columns_add (a write that changes them stops): user_preferences: others. Rows the app adds beside each write, expected exactly that many times: custom_values (2 per write). Tables that gained a different number of rows in different demonstrations; they are not logs unless you list them under logs_add (rows may then be added, existing rows must not change): journal_details. Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): issues: created_on, description, updated_on; time_entries: created_on, tmonth, tweek, tyear, updated_on. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log. - Answer key: `bookkeeping`. Proposed: `accept`. - - Answered: `"accept"` + - Answered: `{"logs_add": ["journal_details"], "columns_add": {"user_preferences": ["others"]}}` + +2. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. + - Answer key: `recheck_after_s`. Proposed: `300`. + - Answered: `300` ## Kinds diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml index 402fc2163f..6c588d8197 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml @@ -1 +1,4 @@ -bookkeeping: accept +bookkeeping: + logs_add: [journal_details] + columns_add: {user_preferences: [others]} +recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml index 8cebbfa15d..cd03ca2408 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml @@ -25,13 +25,20 @@ verifier: AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."firstname" - AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) FROM "users" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" - AS TEXT)) AS "p__assignee", (SELECT CAST(a1."name" AS TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" - AS TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CAST(a1."name" AS TEXT) FROM - "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT - CAST(a1."name" AS TEXT) FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" AS - TEXT)) AS "p__tracker", CASE WHEN t."category_id" IS NULL THEN '1' ELSE '0' END AS "pin__category_id", + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CASE WHEN (SELECT + COUNT(*) FROM "users" u1 WHERE CAST(CAST(u1."firstname" AS TEXT) || ' ' || CAST(u1."lastname" + AS TEXT) AS TEXT) = CAST(CAST(a1."firstname" AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) AS + TEXT)) = 1 THEN CAST(a1."firstname" AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) END FROM "users" + a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" AS TEXT)) AS "p__assignee", (SELECT CASE + WHEN (SELECT COUNT(*) FROM "enumerations" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" + AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "enumerations" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CASE WHEN (SELECT COUNT(*) + FROM "projects" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) + AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) + = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT CASE WHEN (SELECT COUNT(*) FROM "trackers" + u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) AS TEXT)) = 1 THEN + CAST(a1."name" AS TEXT) END FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" + AS TEXT)) AS "p__tracker", CASE WHEN t."category_id" IS NULL THEN '1' ELSE '0' END AS "pin__category_id", CASE WHEN t."closed_on" IS NULL THEN '1' ELSE '0' END AS "pin__closed_on", CASE WHEN CAST(t."done_ratio" AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__done_ratio", CASE WHEN t."estimated_hours" IS NULL THEN '1' ELSE '0' END AS "pin__estimated_hours", CASE WHEN t."fixed_version_id" IS NULL @@ -51,436 +58,26 @@ verifier: sql_password_env: REDMINE_RO_DB_PASSWORD sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CAST(a2."firstname" AS TEXT) || ' ' || CAST(a2."lastname" - AS TEXT) FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" AS TEXT)) FROM - "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", (SELECT CAST(a1."description" + CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "users" u2 + WHERE CAST(CAST(u2."firstname" AS TEXT) || ' ' || CAST(u2."lastname" AS TEXT) AS TEXT) = CAST(CAST(a2."firstname" + AS TEXT) || ' ' || CAST(a2."lastname" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."firstname" AS TEXT) + || ' ' || CAST(a2."lastname" AS TEXT) END FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", (SELECT CAST(a1."description" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__description", (SELECT (SELECT - CAST(a2."name" AS TEXT) FROM "enumerations" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."priority_id" - AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__priority", (SELECT (SELECT - CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" + CASE WHEN (SELECT COUNT(*) FROM "enumerations" u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) + = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" AS TEXT) END FROM "enumerations" + a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."priority_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" + = t."customized_id") AS "main__priority", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" + u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN + CAST(a2."name" AS TEXT) END FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__project", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__subject", (SELECT (SELECT - CAST(a2."name" AS TEXT) FROM "trackers" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."tracker_id" - AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__tracker", CASE WHEN CAST(t."custom_field_id" - AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id", CASE WHEN - CAST(t."custom_field_id" AS numeric) = CAST('2' AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id__47c66b" - FROM "custom_values" t - raw_ar_internal_metadata: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" - AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) - AS "updated_at" FROM "ar_internal_metadata" t - raw_attachments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", - CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", - CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", - CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" - AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) - AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) - AS "disk_directory" FROM "attachments" t - raw_auth_sources: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" - AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" - AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS - TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" - AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" - AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", - CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t - raw_boards: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) - AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) - AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" - AS TEXT) AS "parent_id" FROM "boards" t - raw_changes: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", - CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS - TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS - TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t - raw_changeset_parents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) - AS "parent_id" FROM "changeset_parents" t - raw_changesets: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", - CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" - AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS - TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "changesets" t - raw_changesets_issues: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS - "issue_id" FROM "changesets_issues" t - raw_comments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", - CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", - CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on" FROM "comments" t - raw_custom_field_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) - AS "position" FROM "custom_field_enumerations" t - raw_custom_fields: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" - AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) - AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS - "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", - CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" - AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) - AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", - CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t - raw_custom_fields_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS - TEXT) AS "project_id" FROM "custom_fields_projects" t - raw_custom_fields_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) - AS "role_id" FROM "custom_fields_roles" t - raw_custom_fields_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS - TEXT) AS "tracker_id" FROM "custom_fields_trackers" t - raw_documents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" - AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS - "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t - raw_email_addresses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" - AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) - AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" - FROM "email_addresses" t - raw_enabled_modules: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name" FROM "enabled_modules" t - raw_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS - "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" - t - raw_groups_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "groups_users" t - raw_import_items: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" - AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", - CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t - raw_imports: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS - "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", - CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM - "imports" t - raw_issue_categories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" - t - raw_issue_relations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", - CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", - CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t - raw_issue_statuses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" - AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" - AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" - t - raw_journal_details: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" - AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) - AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t - raw_journals: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", - CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" - AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" - AS TEXT) AS "updated_by_id" FROM "journals" t - raw_member_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" - t - raw_members: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" - AS TEXT) AS "mail_notification" FROM "members" t - raw_messages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS - "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", - CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" - AS TEXT) AS "sticky" FROM "messages" t - raw_news: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS - "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t - raw_oauth_access_grants: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" - AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" - AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t - raw_oauth_access_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" - AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" - AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" - t - raw_oauth_applications: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS - TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", - CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" - t - raw_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" - AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) - AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" - AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" - AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" - AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" - FROM "projects" t - raw_projects_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS - "tracker_id" FROM "projects_trackers" t - raw_queries: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", - CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" - AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) - AS "description" FROM "queries" t - raw_queries_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" - FROM "queries_roles" t - raw_reactions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", - CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t - raw_repositories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" - AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", - CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" - AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" - AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS - TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t - raw_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) - AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) - AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" - AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", - CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS - "default_time_entry_activity_id" FROM "roles" t - raw_roles_managed_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" - FROM "roles_managed_roles" t - raw_schema_migrations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM - "schema_migrations" t - raw_settings: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" - AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t - raw_time_entries: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", - CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" - AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", - CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t - raw_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" - AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t - raw_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" - AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" - AS TEXT) AS "description" FROM "trackers" t + CASE WHEN (SELECT COUNT(*) FROM "trackers" u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" + AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" AS TEXT) END FROM "trackers" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."tracker_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") + AS "main__tracker", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('1' AS numeric) THEN + '1' ELSE '0' END AS "pin__custom_field_id", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('2' + AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id__47c66b" FROM "custom_values" t raw_user_preferences: kind: sql sql_driver: psycopg2 @@ -489,93 +86,6 @@ verifier: sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) AS "time_zone" FROM "user_preferences" t - raw_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" - AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS - TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", - CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", - CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" - AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" - AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", - CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", - CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" - AS TEXT) AS "twofa_required" FROM "users" t - raw_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS - TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS - TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" - AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t - raw_watchers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", - CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM - "watchers" t - raw_wiki_content_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", - CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", - 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS - "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" - FROM "wiki_content_versions" t - raw_wiki_contents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" - AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" - t - raw_wiki_pages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) - AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t - raw_wiki_redirects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS - TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" - t - raw_wikis: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" - AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t - raw_workflows: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" - AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", - CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" - AS TEXT) AS "rule" FROM "workflows" t effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -639,7 +149,6 @@ effects: param: tracker value: param: customer_ref - kind: phone pin__custom_field_id: literal: '1' customized_type: @@ -669,9 +178,27 @@ effects: literal: Issue expected_count: 1 count_new_only: true +- kind: record_written + probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app + match: + id: + literal: '1' + expected_count: 1 frame: - ignore_fields: - - lock_version + target_fields: - others - - updated_on - target_fields: [] + database: + dialect: postgresql + read: + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + allow_tables: + - table: journal_details + - table: custom_values + - table: issues + views: + user_preferences: raw_user_preferences + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml index 9802b204ef..bf4588fca2 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml @@ -25,13 +25,31 @@ verifier: AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."firstname" - AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) FROM "users" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" - AS TEXT)) AS "p__assignee", (SELECT CAST(a1."name" AS TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" - AS TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CAST(a1."name" AS TEXT) FROM - "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT - CAST(a1."name" AS TEXT) FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" AS - TEXT)) AS "p__tracker" FROM "issues" t + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CASE WHEN (SELECT + COUNT(*) FROM "users" u1 WHERE CAST(CAST(u1."firstname" AS TEXT) || ' ' || CAST(u1."lastname" + AS TEXT) AS TEXT) = CAST(CAST(a1."firstname" AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) AS + TEXT)) = 1 THEN CAST(a1."firstname" AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) END FROM "users" + a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" AS TEXT)) AS "p__assignee", (SELECT CASE + WHEN (SELECT COUNT(*) FROM "enumerations" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" + AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "enumerations" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CASE WHEN (SELECT COUNT(*) + FROM "projects" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) + AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) + = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT CASE WHEN (SELECT COUNT(*) FROM "trackers" + u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) AS TEXT)) = 1 THEN + CAST(a1."name" AS TEXT) END FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" + AS TEXT)) AS "p__tracker", CASE WHEN t."category_id" IS NULL THEN '1' ELSE '0' END AS "pin__category_id", + CASE WHEN t."closed_on" IS NULL THEN '1' ELSE '0' END AS "pin__closed_on", CASE WHEN CAST(t."done_ratio" + AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__done_ratio", CASE WHEN t."estimated_hours" + IS NULL THEN '1' ELSE '0' END AS "pin__estimated_hours", CASE WHEN t."fixed_version_id" IS NULL + THEN '1' ELSE '0' END AS "pin__fixed_version_id", CASE WHEN CAST(CAST(t."is_private" AS integer) + AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__is_private", CASE WHEN CAST(t."lft" + AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__lft", CASE WHEN CAST(t."lock_version" + AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__lock_version", CASE WHEN t."parent_id" + IS NULL THEN '1' ELSE '0' END AS "pin__parent_id", CASE WHEN CAST(t."rgt" AS numeric) = CAST('2' + AS numeric) THEN '1' ELSE '0' END AS "pin__rgt", CASE WHEN t."start_date" IS NULL THEN '1' ELSE + '0' END AS "pin__start_date", CASE WHEN CAST(t."status_id" AS numeric) = CAST('1' AS numeric) + THEN '1' ELSE '0' END AS "pin__status_id" FROM "issues" t rows_custom_values: kind: sql sql_driver: psycopg2 @@ -39,434 +57,26 @@ verifier: sql_password_env: REDMINE_RO_DB_PASSWORD sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CAST(a2."firstname" AS TEXT) || ' ' || CAST(a2."lastname" - AS TEXT) FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" AS TEXT)) FROM - "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", (SELECT CAST(a1."description" + CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "users" u2 + WHERE CAST(CAST(u2."firstname" AS TEXT) || ' ' || CAST(u2."lastname" AS TEXT) AS TEXT) = CAST(CAST(a2."firstname" + AS TEXT) || ' ' || CAST(a2."lastname" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."firstname" AS TEXT) + || ' ' || CAST(a2."lastname" AS TEXT) END FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" + AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", (SELECT CAST(a1."description" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__description", (SELECT (SELECT - CAST(a2."name" AS TEXT) FROM "enumerations" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."priority_id" - AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__priority", (SELECT (SELECT - CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" + CASE WHEN (SELECT COUNT(*) FROM "enumerations" u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) + = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" AS TEXT) END FROM "enumerations" + a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."priority_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" + = t."customized_id") AS "main__priority", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" + u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN + CAST(a2."name" AS TEXT) END FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__project", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__subject", (SELECT (SELECT - CAST(a2."name" AS TEXT) FROM "trackers" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."tracker_id" - AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__tracker" FROM "custom_values" - t - raw_ar_internal_metadata: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" - AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) - AS "updated_at" FROM "ar_internal_metadata" t - raw_attachments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", - CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", - CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", - CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" - AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) - AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) - AS "disk_directory" FROM "attachments" t - raw_auth_sources: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" - AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" - AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS - TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" - AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" - AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", - CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t - raw_boards: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) - AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) - AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" - AS TEXT) AS "parent_id" FROM "boards" t - raw_changes: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", - CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS - TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS - TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t - raw_changeset_parents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) - AS "parent_id" FROM "changeset_parents" t - raw_changesets: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", - CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" - AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS - TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "changesets" t - raw_changesets_issues: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS - "issue_id" FROM "changesets_issues" t - raw_comments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", - CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", - CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on" FROM "comments" t - raw_custom_field_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) - AS "position" FROM "custom_field_enumerations" t - raw_custom_fields: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" - AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) - AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS - "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", - CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" - AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) - AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", - CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t - raw_custom_fields_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS - TEXT) AS "project_id" FROM "custom_fields_projects" t - raw_custom_fields_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) - AS "role_id" FROM "custom_fields_roles" t - raw_custom_fields_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS - TEXT) AS "tracker_id" FROM "custom_fields_trackers" t - raw_documents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" - AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS - "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t - raw_email_addresses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" - AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) - AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" - FROM "email_addresses" t - raw_enabled_modules: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name" FROM "enabled_modules" t - raw_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS - "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" - t - raw_groups_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "groups_users" t - raw_import_items: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" - AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", - CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t - raw_imports: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS - "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", - CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM - "imports" t - raw_issue_categories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" - t - raw_issue_relations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", - CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", - CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t - raw_issue_statuses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" - AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" - AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" - t - raw_journal_details: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" - AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) - AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t - raw_journals: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", - CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" - AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" - AS TEXT) AS "updated_by_id" FROM "journals" t - raw_member_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" - t - raw_members: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" - AS TEXT) AS "mail_notification" FROM "members" t - raw_messages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS - "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", - CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" - AS TEXT) AS "sticky" FROM "messages" t - raw_news: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS - "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t - raw_oauth_access_grants: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" - AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" - AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t - raw_oauth_access_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" - AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" - AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" - t - raw_oauth_applications: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS - TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", - CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" - t - raw_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" - AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) - AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" - AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" - AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" - AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" - FROM "projects" t - raw_projects_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS - "tracker_id" FROM "projects_trackers" t - raw_queries: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", - CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" - AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) - AS "description" FROM "queries" t - raw_queries_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" - FROM "queries_roles" t - raw_reactions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", - CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t - raw_repositories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" - AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", - CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" - AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" - AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS - TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t - raw_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) - AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) - AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" - AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", - CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS - "default_time_entry_activity_id" FROM "roles" t - raw_roles_managed_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" - FROM "roles_managed_roles" t - raw_schema_migrations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM - "schema_migrations" t - raw_settings: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" - AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t - raw_time_entries: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", - CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" - AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", - CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t - raw_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" - AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t - raw_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" - AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" - AS TEXT) AS "description" FROM "trackers" t + CASE WHEN (SELECT COUNT(*) FROM "trackers" u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" + AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" AS TEXT) END FROM "trackers" a2 WHERE CAST(a2."id" + AS TEXT) = CAST(a1."tracker_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") + AS "main__tracker", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('1' AS numeric) THEN + '1' ELSE '0' END AS "pin__custom_field_id", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('2' + AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id__47c66b" FROM "custom_values" t raw_user_preferences: kind: sql sql_driver: psycopg2 @@ -475,93 +85,6 @@ verifier: sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) AS "time_zone" FROM "user_preferences" t - raw_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" - AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS - TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", - CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", - CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" - AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" - AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", - CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", - CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" - AS TEXT) AS "twofa_required" FROM "users" t - raw_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS - TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS - TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" - AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t - raw_watchers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", - CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM - "watchers" t - raw_wiki_content_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", - CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", - 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS - "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" - FROM "wiki_content_versions" t - raw_wiki_contents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" - AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" - t - raw_wiki_pages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) - AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t - raw_wiki_redirects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS - TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" - t - raw_wikis: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" - AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t - raw_workflows: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" - AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", - CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" - AS TEXT) AS "rule" FROM "workflows" t effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -578,6 +101,30 @@ effects: param: subject p__tracker: param: tracker + pin__category_id: + literal: '1' + pin__closed_on: + literal: '1' + pin__done_ratio: + literal: '1' + pin__estimated_hours: + literal: '1' + pin__fixed_version_id: + literal: '1' + pin__is_private: + literal: '1' + pin__lft: + literal: '1' + pin__lock_version: + literal: '1' + pin__parent_id: + literal: '1' + pin__rgt: + literal: '1' + pin__start_date: + literal: '1' + pin__status_id: + literal: '1' expected_count: 1 - kind: record_written probe: surface=rows_custom_values|a row beside the main record holds customer_ref @@ -596,7 +143,10 @@ effects: param: tracker value: param: customer_ref - kind: phone + pin__custom_field_id: + literal: '1' + customized_type: + literal: Issue expected_count: 1 - kind: record_written probe: surface=rows_custom_values|a row beside the main record holds environment @@ -615,10 +165,32 @@ effects: param: tracker value: param: environment + pin__custom_field_id__47c66b: + literal: '1' + customized_type: + literal: Issue + expected_count: 1 +- kind: record_written + probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app + match: + id: + literal: '1' expected_count: 1 frame: - ignore_fields: - - lock_version + target_fields: - others - - updated_on - target_fields: [] + database: + dialect: postgresql + read: + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + allow_tables: + - table: journal_details + - table: custom_values + - table: issues + views: + user_preferences: raw_user_preferences + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml index a8e88de096..ac628cdc8e 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml @@ -20,447 +20,23 @@ verifier: CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS - TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", - CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", - (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."issue_id" - AS TEXT)) AS "p__issue_subject", (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" - AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s - AS date) THEN '1' ELSE '0' END AS "p__spent_on" FROM "time_entries" t + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CASE WHEN (SELECT + COUNT(*) FROM "enumerations" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" + AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "enumerations" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", CASE WHEN CAST(t."hours" AS numeric) + = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", (SELECT CASE WHEN (SELECT COUNT(*) + FROM "issues" u1 WHERE CAST(CAST(u1."subject" AS TEXT) AS TEXT) = CAST(CAST(a1."subject" AS TEXT) + AS TEXT)) = 1 THEN CAST(a1."subject" AS TEXT) END FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) + = CAST(t."issue_id" AS TEXT)) AS "p__issue_subject", (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" + u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) AS TEXT)) = 1 THEN + CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" + AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END + AS "p__spent_on" FROM "time_entries" t sql_query_params: v0: param: hours v1: param: spent_on - raw_ar_internal_metadata: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" - AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) - AS "updated_at" FROM "ar_internal_metadata" t - raw_attachments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", - CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", - CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", - CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" - AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) - AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) - AS "disk_directory" FROM "attachments" t - raw_auth_sources: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" - AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" - AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS - TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" - AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" - AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", - CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t - raw_boards: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) - AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) - AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" - AS TEXT) AS "parent_id" FROM "boards" t - raw_changes: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", - CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS - TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS - TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t - raw_changeset_parents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) - AS "parent_id" FROM "changeset_parents" t - raw_changesets: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", - CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" - AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS - TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "changesets" t - raw_changesets_issues: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS - "issue_id" FROM "changesets_issues" t - raw_comments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", - CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", - CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on" FROM "comments" t - raw_custom_field_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) - AS "position" FROM "custom_field_enumerations" t - raw_custom_fields: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" - AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) - AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS - "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", - CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" - AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) - AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", - CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t - raw_custom_fields_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS - TEXT) AS "project_id" FROM "custom_fields_projects" t - raw_custom_fields_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) - AS "role_id" FROM "custom_fields_roles" t - raw_custom_fields_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS - TEXT) AS "tracker_id" FROM "custom_fields_trackers" t - raw_custom_values: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", - CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t - raw_documents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" - AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS - "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t - raw_email_addresses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" - AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) - AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" - FROM "email_addresses" t - raw_enabled_modules: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name" FROM "enabled_modules" t - raw_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS - "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" - t - raw_groups_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "groups_users" t - raw_import_items: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" - AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", - CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t - raw_imports: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS - "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", - CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM - "imports" t - raw_issue_categories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" - t - raw_issue_relations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", - CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", - CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t - raw_issue_statuses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" - AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" - AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" - t - raw_issues: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) - AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", - CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", - CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", - CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", - CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" - AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" - AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS - TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t - raw_journal_details: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" - AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) - AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t - raw_journals: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", - CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" - AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" - AS TEXT) AS "updated_by_id" FROM "journals" t - raw_member_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" - t - raw_members: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" - AS TEXT) AS "mail_notification" FROM "members" t - raw_messages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS - "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", - CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" - AS TEXT) AS "sticky" FROM "messages" t - raw_news: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS - "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t - raw_oauth_access_grants: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" - AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" - AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t - raw_oauth_access_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" - AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" - AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" - t - raw_oauth_applications: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS - TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", - CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" - t - raw_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" - AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) - AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" - AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" - AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" - AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" - FROM "projects" t - raw_projects_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS - "tracker_id" FROM "projects_trackers" t - raw_queries: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", - CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" - AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) - AS "description" FROM "queries" t - raw_queries_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" - FROM "queries_roles" t - raw_reactions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", - CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t - raw_repositories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" - AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", - CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" - AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" - AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS - TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t - raw_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) - AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) - AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" - AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", - CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS - "default_time_entry_activity_id" FROM "roles" t - raw_roles_managed_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" - FROM "roles_managed_roles" t - raw_schema_migrations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM - "schema_migrations" t - raw_settings: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" - AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t - raw_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" - AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t - raw_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" - AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" - AS TEXT) AS "description" FROM "trackers" t raw_user_preferences: kind: sql sql_driver: psycopg2 @@ -469,93 +45,6 @@ verifier: sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) AS "time_zone" FROM "user_preferences" t - raw_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" - AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS - TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", - CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", - CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" - AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" - AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", - CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", - CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" - AS TEXT) AS "twofa_required" FROM "users" t - raw_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS - TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS - TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" - AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t - raw_watchers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", - CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM - "watchers" t - raw_wiki_content_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", - CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", - 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS - "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" - FROM "wiki_content_versions" t - raw_wiki_contents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" - AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" - t - raw_wiki_pages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) - AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t - raw_wiki_redirects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS - TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" - t - raw_wikis: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" - AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t - raw_workflows: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" - AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", - CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" - AS TEXT) AS "rule" FROM "workflows" t effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -578,9 +67,26 @@ effects: param: bot_user_id expected_count: 1 count_new_only: true +- kind: record_written + probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app + match: + id: + literal: '1' + expected_count: 1 frame: - ignore_fields: - - lock_version + target_fields: - others - - updated_on - target_fields: [] + database: + dialect: postgresql + read: + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + allow_tables: + - table: journal_details + - table: time_entries + views: + user_preferences: raw_user_preferences + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml index 93e2caefcc..666e1672db 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml @@ -20,447 +20,23 @@ verifier: CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CAST(a1."name" AS - TEXT) FROM "enumerations" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", - CASE WHEN CAST(t."hours" AS numeric) = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", - (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."issue_id" - AS TEXT)) AS "p__issue_subject", (SELECT CAST(a1."name" AS TEXT) FROM "projects" a1 WHERE CAST(a1."id" - AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s - AS date) THEN '1' ELSE '0' END AS "p__spent_on" FROM "time_entries" t + AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CASE WHEN (SELECT + COUNT(*) FROM "enumerations" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" + AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "enumerations" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", CASE WHEN CAST(t."hours" AS numeric) + = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", (SELECT CASE WHEN (SELECT COUNT(*) + FROM "issues" u1 WHERE CAST(CAST(u1."subject" AS TEXT) AS TEXT) = CAST(CAST(a1."subject" AS TEXT) + AS TEXT)) = 1 THEN CAST(a1."subject" AS TEXT) END FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) + = CAST(t."issue_id" AS TEXT)) AS "p__issue_subject", (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" + u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) AS TEXT)) = 1 THEN + CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" + AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END + AS "p__spent_on" FROM "time_entries" t sql_query_params: v0: param: hours v1: param: spent_on - raw_ar_internal_metadata: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" - AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) - AS "updated_at" FROM "ar_internal_metadata" t - raw_attachments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", - CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", - CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", - CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" - AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) - AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) - AS "disk_directory" FROM "attachments" t - raw_auth_sources: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" - AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" - AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS - TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" - AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" - AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", - CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t - raw_boards: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) - AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) - AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" - AS TEXT) AS "parent_id" FROM "boards" t - raw_changes: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", - CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS - TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS - TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t - raw_changeset_parents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) - AS "parent_id" FROM "changeset_parents" t - raw_changesets: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", - CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" - AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS - TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "changesets" t - raw_changesets_issues: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS - "issue_id" FROM "changesets_issues" t - raw_comments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", - CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", - CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on" FROM "comments" t - raw_custom_field_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) - AS "position" FROM "custom_field_enumerations" t - raw_custom_fields: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" - AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) - AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS - "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", - CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" - AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) - AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", - CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t - raw_custom_fields_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS - TEXT) AS "project_id" FROM "custom_fields_projects" t - raw_custom_fields_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) - AS "role_id" FROM "custom_fields_roles" t - raw_custom_fields_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS - TEXT) AS "tracker_id" FROM "custom_fields_trackers" t - raw_custom_values: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", - CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t - raw_documents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" - AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS - "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t - raw_email_addresses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" - AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) - AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" - FROM "email_addresses" t - raw_enabled_modules: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name" FROM "enabled_modules" t - raw_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS - "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" - t - raw_groups_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "groups_users" t - raw_import_items: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" - AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", - CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t - raw_imports: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS - "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", - CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM - "imports" t - raw_issue_categories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" - t - raw_issue_relations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", - CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", - CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t - raw_issue_statuses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" - AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" - AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" - t - raw_issues: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) - AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", - CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", - CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", - CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", - CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" - AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" - AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS - TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on" FROM "issues" t - raw_journal_details: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journal_id" AS TEXT) AS "journal_id", CAST(t."property" - AS TEXT) AS "property", CAST(t."prop_key" AS TEXT) AS "prop_key", CAST(t."old_value" AS TEXT) - AS "old_value", CAST(t."value" AS TEXT) AS "value" FROM "journal_details" t - raw_journals: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", - CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" - AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" - AS TEXT) AS "updated_by_id" FROM "journals" t - raw_member_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" - t - raw_members: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" - AS TEXT) AS "mail_notification" FROM "members" t - raw_messages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS - "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", - CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" - AS TEXT) AS "sticky" FROM "messages" t - raw_news: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS - "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t - raw_oauth_access_grants: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" - AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" - AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t - raw_oauth_access_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" - AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" - AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" - t - raw_oauth_applications: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS - TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", - CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" - t - raw_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" - AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) - AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" - AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" - AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" - AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" - FROM "projects" t - raw_projects_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS - "tracker_id" FROM "projects_trackers" t - raw_queries: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", - CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" - AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) - AS "description" FROM "queries" t - raw_queries_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" - FROM "queries_roles" t - raw_reactions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", - CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t - raw_repositories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" - AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", - CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" - AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" - AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS - TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t - raw_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) - AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) - AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" - AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", - CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS - "default_time_entry_activity_id" FROM "roles" t - raw_roles_managed_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" - FROM "roles_managed_roles" t - raw_schema_migrations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM - "schema_migrations" t - raw_settings: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" - AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t - raw_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" - AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t - raw_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" - AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" - AS TEXT) AS "description" FROM "trackers" t raw_user_preferences: kind: sql sql_driver: psycopg2 @@ -469,93 +45,6 @@ verifier: sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) AS "time_zone" FROM "user_preferences" t - raw_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" - AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS - TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", - CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", - CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" - AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" - AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", - CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", - CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" - AS TEXT) AS "twofa_required" FROM "users" t - raw_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS - TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS - TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" - AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t - raw_watchers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", - CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM - "watchers" t - raw_wiki_content_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", - CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", - 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS - "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" - FROM "wiki_content_versions" t - raw_wiki_contents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" - AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" - t - raw_wiki_pages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) - AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t - raw_wiki_redirects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS - TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" - t - raw_wikis: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" - AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t - raw_workflows: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" - AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", - CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" - AS TEXT) AS "rule" FROM "workflows" t effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -573,9 +62,26 @@ effects: p__spent_on: literal: '1' expected_count: 1 +- kind: record_written + probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app + match: + id: + literal: '1' + expected_count: 1 frame: - ignore_fields: - - lock_version + target_fields: - others - - updated_on - target_fields: [] + database: + dialect: postgresql + read: + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + allow_tables: + - table: journal_details + - table: time_entries + views: + user_preferences: raw_user_preferences + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml index 95a96cf13c..8646bc39d6 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml @@ -25,10 +25,13 @@ verifier: AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."name" AS - TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", - (SELECT CAST(a1."name" AS TEXT) FROM "issue_statuses" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."status_id" - AS TEXT)) AS "p__status" FROM "issues" t + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CASE WHEN (SELECT + COUNT(*) FROM "projects" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" + AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT CASE WHEN (SELECT COUNT(*) FROM + "issue_statuses" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) + AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "issue_statuses" a1 WHERE CAST(a1."id" AS + TEXT) = CAST(t."status_id" AS TEXT)) AS "p__status" FROM "issues" t rows_journals: kind: sql sql_driver: psycopg2 @@ -38,418 +41,14 @@ verifier: CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" - AS TEXT) AS "updated_by_id", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE - CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") - AS "main__project", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "issue_statuses" a2 WHERE CAST(a2."id" - AS TEXT) = CAST(a1."status_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") - AS "main__status", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."journalized_id") - AS "main__subject" FROM "journals" t - raw_ar_internal_metadata: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" - AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) - AS "updated_at" FROM "ar_internal_metadata" t - raw_attachments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", - CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", - CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", - CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" - AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) - AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) - AS "disk_directory" FROM "attachments" t - raw_auth_sources: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" - AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" - AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS - TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" - AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" - AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", - CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t - raw_boards: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) - AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) - AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" - AS TEXT) AS "parent_id" FROM "boards" t - raw_changes: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", - CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS - TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS - TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t - raw_changeset_parents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) - AS "parent_id" FROM "changeset_parents" t - raw_changesets: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", - CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" - AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS - TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "changesets" t - raw_changesets_issues: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS - "issue_id" FROM "changesets_issues" t - raw_comments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", - CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", - CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on" FROM "comments" t - raw_custom_field_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) - AS "position" FROM "custom_field_enumerations" t - raw_custom_fields: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" - AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) - AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS - "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", - CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" - AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) - AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", - CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t - raw_custom_fields_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS - TEXT) AS "project_id" FROM "custom_fields_projects" t - raw_custom_fields_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) - AS "role_id" FROM "custom_fields_roles" t - raw_custom_fields_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS - TEXT) AS "tracker_id" FROM "custom_fields_trackers" t - raw_custom_values: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", - CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t - raw_documents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" - AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS - "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t - raw_email_addresses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" - AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) - AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" - FROM "email_addresses" t - raw_enabled_modules: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name" FROM "enabled_modules" t - raw_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS - "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" - t - raw_groups_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "groups_users" t - raw_import_items: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" - AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", - CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t - raw_imports: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS - "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", - CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM - "imports" t - raw_issue_categories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" - t - raw_issue_relations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", - CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", - CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t - raw_issue_statuses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" - AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" - AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" - t - raw_member_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" - t - raw_members: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" - AS TEXT) AS "mail_notification" FROM "members" t - raw_messages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS - "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", - CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" - AS TEXT) AS "sticky" FROM "messages" t - raw_news: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS - "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t - raw_oauth_access_grants: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" - AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" - AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t - raw_oauth_access_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" - AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" - AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" - t - raw_oauth_applications: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS - TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", - CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" - t - raw_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" - AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) - AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" - AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" - AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" - AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" - FROM "projects" t - raw_projects_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS - "tracker_id" FROM "projects_trackers" t - raw_queries: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", - CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" - AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) - AS "description" FROM "queries" t - raw_queries_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" - FROM "queries_roles" t - raw_reactions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", - CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t - raw_repositories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" - AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", - CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" - AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" - AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS - TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t - raw_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) - AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) - AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" - AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", - CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS - "default_time_entry_activity_id" FROM "roles" t - raw_roles_managed_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" - FROM "roles_managed_roles" t - raw_schema_migrations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM - "schema_migrations" t - raw_settings: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" - AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t - raw_time_entries: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", - CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" - AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", - CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t - raw_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" - AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t - raw_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" - AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" - AS TEXT) AS "description" FROM "trackers" t + AS TEXT) AS "updated_by_id", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" u2 WHERE + CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" + AS TEXT) END FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM + "issues" a1 WHERE a1."id" = t."journalized_id") AS "main__project", (SELECT CAST(a1."subject" + AS TEXT) FROM "issues" a1 WHERE a1."id" = t."journalized_id") AS "main__subject", CASE WHEN CAST(CAST(t."private_notes" + AS integer) AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__private_notes", + CASE WHEN t."updated_by_id" IS NULL THEN '1' ELSE '0' END AS "pin__updated_by_id", CASE WHEN CAST(t."user_id" + AS numeric) = CAST('5' AS numeric) THEN '1' ELSE '0' END AS "pin__user_id" FROM "journals" t raw_user_preferences: kind: sql sql_driver: psycopg2 @@ -458,93 +57,6 @@ verifier: sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) AS "time_zone" FROM "user_preferences" t - raw_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" - AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS - TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", - CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", - CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" - AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" - AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", - CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", - CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" - AS TEXT) AS "twofa_required" FROM "users" t - raw_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS - TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS - TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" - AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t - raw_watchers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", - CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM - "watchers" t - raw_wiki_content_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", - CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", - 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS - "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" - FROM "wiki_content_versions" t - raw_wiki_contents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" - AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" - t - raw_wiki_pages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) - AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t - raw_wiki_redirects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS - TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" - t - raw_wikis: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" - AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t - raw_workflows: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" - AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", - CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" - AS TEXT) AS "rule" FROM "workflows" t effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -561,19 +73,28 @@ effects: match: main__project: param: project - main__status: - param: status main__subject: param: subject notes: param: notes + journalized_type: + literal: Issue + pin__private_notes: + literal: '1' + pin__updated_by_id: + literal: '1' + pin__user_id: + literal: '1' + expected_count: 1 +- kind: record_written + probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app + match: + id: + literal: '1' expected_count: 1 frame: - ignore_fields: - - lock_version + target_fields: - others - - updated_on - target_fields: [] versions: read: kind: sql @@ -588,3 +109,18 @@ frame: author_field: author self: - param: bot_user_id + database: + dialect: postgresql + read: + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + allow_tables: + - table: journal_details + - table: issues + - table: journals + views: + user_preferences: raw_user_preferences + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml index 295dcbaf95..0cc306597c 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml +++ b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml @@ -25,10 +25,13 @@ verifier: AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CAST(a1."name" AS - TEXT) FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", - (SELECT CAST(a1."name" AS TEXT) FROM "issue_statuses" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."status_id" - AS TEXT)) AS "p__status" FROM "issues" t + AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CASE WHEN (SELECT + COUNT(*) FROM "projects" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" + AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" + AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT CASE WHEN (SELECT COUNT(*) FROM + "issue_statuses" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) + AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "issue_statuses" a1 WHERE CAST(a1."id" AS + TEXT) = CAST(t."status_id" AS TEXT)) AS "p__status" FROM "issues" t rows_journals: kind: sql sql_driver: psycopg2 @@ -38,421 +41,14 @@ verifier: CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" - AS TEXT) AS "updated_by_id", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "projects" a2 WHERE - CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") - AS "main__project", (SELECT (SELECT CAST(a2."name" AS TEXT) FROM "issue_statuses" a2 WHERE CAST(a2."id" - AS TEXT) = CAST(a1."status_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."journalized_id") - AS "main__status", (SELECT CAST(a1."subject" AS TEXT) FROM "issues" a1 WHERE a1."id" = t."journalized_id") - AS "main__subject", CASE WHEN CAST(CAST(t."private_notes" AS integer) AS numeric) = CAST('0' AS - numeric) THEN '1' ELSE '0' END AS "pin__private_notes", CASE WHEN t."updated_by_id" IS NULL THEN - '1' ELSE '0' END AS "pin__updated_by_id", CASE WHEN CAST(t."user_id" AS numeric) = CAST('5' AS - numeric) THEN '1' ELSE '0' END AS "pin__user_id" FROM "journals" t - raw_ar_internal_metadata: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."key" AS TEXT) AS "id", CAST(t."key" AS TEXT) AS "key", CAST(t."value" - AS TEXT) AS "value", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) - AS "updated_at" FROM "ar_internal_metadata" t - raw_attachments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."container_id" AS TEXT) AS "container_id", - CAST(t."container_type" AS TEXT) AS "container_type", CAST(t."filename" AS TEXT) AS "filename", - CAST(t."disk_filename" AS TEXT) AS "disk_filename", CAST(t."filesize" AS TEXT) AS "filesize", - CAST(t."content_type" AS TEXT) AS "content_type", CAST(t."digest" AS TEXT) AS "digest", CAST(t."downloads" - AS TEXT) AS "downloads", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) - AS "created_on", CAST(t."description" AS TEXT) AS "description", CAST(t."disk_directory" AS TEXT) - AS "disk_directory" FROM "attachments" t - raw_auth_sources: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."host" AS TEXT) AS "host", CAST(t."port" AS TEXT) AS "port", CAST(t."account" - AS TEXT) AS "account", CAST(t."account_password" AS TEXT) AS "account_password", CAST(t."base_dn" - AS TEXT) AS "base_dn", CAST(t."attr_login" AS TEXT) AS "attr_login", CAST(t."attr_firstname" AS - TEXT) AS "attr_firstname", CAST(t."attr_lastname" AS TEXT) AS "attr_lastname", CAST(t."attr_mail" - AS TEXT) AS "attr_mail", CAST(t."onthefly_register" AS TEXT) AS "onthefly_register", CAST(t."tls" - AS TEXT) AS "tls", CAST(t."filter" AS TEXT) AS "filter", CAST(t."timeout" AS TEXT) AS "timeout", - CAST(t."verify_peer" AS TEXT) AS "verify_peer" FROM "auth_sources" t - raw_boards: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."position" AS TEXT) - AS "position", CAST(t."topics_count" AS TEXT) AS "topics_count", CAST(t."messages_count" AS TEXT) - AS "messages_count", CAST(t."last_message_id" AS TEXT) AS "last_message_id", CAST(t."parent_id" - AS TEXT) AS "parent_id" FROM "boards" t - raw_changes: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."changeset_id" AS TEXT) AS "changeset_id", - CAST(t."action" AS TEXT) AS "action", CAST(t."path" AS TEXT) AS "path", CAST(t."from_path" AS - TEXT) AS "from_path", CAST(t."from_revision" AS TEXT) AS "from_revision", CAST(t."revision" AS - TEXT) AS "revision", CAST(t."branch" AS TEXT) AS "branch" FROM "changes" t - raw_changeset_parents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."parent_id" AS TEXT) - AS "parent_id" FROM "changeset_parents" t - raw_changesets: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."repository_id" AS TEXT) AS "repository_id", - CAST(t."revision" AS TEXT) AS "revision", CAST(t."committer" AS TEXT) AS "committer", CAST(t."committed_on" - AS TEXT) AS "committed_on", CAST(t."comments" AS TEXT) AS "comments", CAST(t."commit_date" AS - TEXT) AS "commit_date", CAST(t."scmid" AS TEXT) AS "scmid", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "changesets" t - raw_changesets_issues: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."changeset_id" AS TEXT) AS "changeset_id", CAST(t."issue_id" AS TEXT) AS - "issue_id" FROM "changesets_issues" t - raw_comments: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."commented_type" AS TEXT) AS "commented_type", - CAST(t."commented_id" AS TEXT) AS "commented_id", CAST(t."author_id" AS TEXT) AS "author_id", - CAST(t."content" AS TEXT) AS "content", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on" FROM "comments" t - raw_custom_field_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."name" AS TEXT) AS "name", CAST(t."active" AS TEXT) AS "active", CAST(t."position" AS TEXT) - AS "position" FROM "custom_field_enumerations" t - raw_custom_fields: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."name" - AS TEXT) AS "name", CAST(t."field_format" AS TEXT) AS "field_format", CAST(t."possible_values" - AS TEXT) AS "possible_values", CAST(t."regexp" AS TEXT) AS "regexp", CAST(t."min_length" AS TEXT) - AS "min_length", CAST(t."max_length" AS TEXT) AS "max_length", CAST(t."is_required" AS TEXT) AS - "is_required", CAST(t."is_for_all" AS TEXT) AS "is_for_all", CAST(t."is_filter" AS TEXT) AS "is_filter", - CAST(t."position" AS TEXT) AS "position", CAST(t."searchable" AS TEXT) AS "searchable", CAST(t."default_value" - AS TEXT) AS "default_value", CAST(t."editable" AS TEXT) AS "editable", CAST(t."visible" AS TEXT) - AS "visible", CAST(t."multiple" AS TEXT) AS "multiple", CAST(t."format_store" AS TEXT) AS "format_store", - CAST(t."description" AS TEXT) AS "description" FROM "custom_fields" t - raw_custom_fields_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."project_id" AS - TEXT) AS "project_id" FROM "custom_fields_projects" t - raw_custom_fields_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."role_id" AS TEXT) - AS "role_id" FROM "custom_fields_roles" t - raw_custom_fields_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", CAST(t."tracker_id" AS - TEXT) AS "tracker_id" FROM "custom_fields_trackers" t - raw_custom_values: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", - CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."value" AS TEXT) AS "value" FROM "custom_values" t - raw_documents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."category_id" - AS TEXT) AS "category_id", CAST(t."title" AS TEXT) AS "title", CAST(t."description" AS TEXT) AS - "description", CAST(t."created_on" AS TEXT) AS "created_on" FROM "documents" t - raw_email_addresses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."address" - AS TEXT) AS "address", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."notify" AS TEXT) - AS "notify", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on" - FROM "email_addresses" t - raw_enabled_modules: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name" FROM "enabled_modules" t - raw_enumerations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_default" AS TEXT) AS "is_default", CAST(t."type" AS TEXT) AS - "type", CAST(t."active" AS TEXT) AS "active", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."position_name" AS TEXT) AS "position_name" FROM "enumerations" - t - raw_groups_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."group_id" AS TEXT) AS "group_id", CAST(t."user_id" AS TEXT) AS "user_id" - FROM "groups_users" t - raw_import_items: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."import_id" AS TEXT) AS "import_id", CAST(t."position" - AS TEXT) AS "position", CAST(t."obj_id" AS TEXT) AS "obj_id", CAST(t."message" AS TEXT) AS "message", - CAST(t."unique_id" AS TEXT) AS "unique_id" FROM "import_items" t - raw_imports: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."type" AS TEXT) AS "type", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."filename" AS TEXT) AS "filename", CAST(t."settings" AS TEXT) AS - "settings", CAST(t."total_items" AS TEXT) AS "total_items", CAST(t."finished" AS TEXT) AS "finished", - CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM - "imports" t - raw_issue_categories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id" FROM "issue_categories" - t - raw_issue_relations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."issue_from_id" AS TEXT) AS "issue_from_id", - CAST(t."issue_to_id" AS TEXT) AS "issue_to_id", CAST(t."relation_type" AS TEXT) AS "relation_type", - CAST(t."delay" AS TEXT) AS "delay" FROM "issue_relations" t - raw_issue_statuses: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."is_closed" - AS TEXT) AS "is_closed", CAST(t."position" AS TEXT) AS "position", CAST(t."default_done_ratio" - AS TEXT) AS "default_done_ratio", CAST(t."description" AS TEXT) AS "description" FROM "issue_statuses" - t - raw_member_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."member_id" AS TEXT) AS "member_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."inherited_from" AS TEXT) AS "inherited_from" FROM "member_roles" - t - raw_members: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."mail_notification" - AS TEXT) AS "mail_notification" FROM "members" t - raw_messages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."board_id" AS TEXT) AS "board_id", CAST(t."parent_id" - AS TEXT) AS "parent_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."content" AS TEXT) AS - "content", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."replies_count" AS TEXT) AS "replies_count", - CAST(t."last_reply_id" AS TEXT) AS "last_reply_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."locked" AS TEXT) AS "locked", CAST(t."sticky" - AS TEXT) AS "sticky" FROM "messages" t - raw_news: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."summary" AS TEXT) AS "summary", CAST(t."description" AS TEXT) AS - "description", CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."comments_count" AS TEXT) AS "comments_count" FROM "news" t - raw_oauth_access_grants: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."expires_in" - AS TEXT) AS "expires_in", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."revoked_at" AS TEXT) AS "revoked_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."code_challenge" AS TEXT) AS "code_challenge", CAST(t."code_challenge_method" - AS TEXT) AS "code_challenge_method" FROM "oauth_access_grants" t - raw_oauth_access_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."resource_owner_id" AS TEXT) AS "resource_owner_id", - CAST(t."application_id" AS TEXT) AS "application_id", CAST(t."token" AS TEXT) AS "token", CAST(t."refresh_token" - AS TEXT) AS "refresh_token", CAST(t."expires_in" AS TEXT) AS "expires_in", CAST(t."revoked_at" - AS TEXT) AS "revoked_at", CAST(t."created_at" AS TEXT) AS "created_at", CAST(t."scopes" AS TEXT) - AS "scopes", CAST(t."previous_refresh_token" AS TEXT) AS "previous_refresh_token" FROM "oauth_access_tokens" - t - raw_oauth_applications: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."uid" AS - TEXT) AS "uid", CAST(t."secret" AS TEXT) AS "secret", CAST(t."redirect_uri" AS TEXT) AS "redirect_uri", - CAST(t."scopes" AS TEXT) AS "scopes", CAST(t."confidential" AS TEXT) AS "confidential", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "oauth_applications" - t - raw_projects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."description" - AS TEXT) AS "description", CAST(t."homepage" AS TEXT) AS "homepage", CAST(t."is_public" AS TEXT) - AS "is_public", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."status" - AS TEXT) AS "status", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."inherit_members" - AS TEXT) AS "inherit_members", CAST(t."default_version_id" AS TEXT) AS "default_version_id", CAST(t."default_assigned_to_id" - AS TEXT) AS "default_assigned_to_id", CAST(t."default_issue_query_id" AS TEXT) AS "default_issue_query_id" - FROM "projects" t - raw_projects_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."tracker_id" AS TEXT) AS - "tracker_id" FROM "projects_trackers" t - raw_queries: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."filters" AS TEXT) AS "filters", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."column_names" AS TEXT) AS "column_names", CAST(t."sort_criteria" AS TEXT) AS "sort_criteria", - CAST(t."group_by" AS TEXT) AS "group_by", CAST(t."type" AS TEXT) AS "type", CAST(t."visibility" - AS TEXT) AS "visibility", CAST(t."options" AS TEXT) AS "options", CAST(t."description" AS TEXT) - AS "description" FROM "queries" t - raw_queries_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."query_id" AS TEXT) AS "query_id", CAST(t."role_id" AS TEXT) AS "role_id" - FROM "queries_roles" t - raw_reactions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."reactable_type" AS TEXT) AS "reactable_type", - CAST(t."reactable_id" AS TEXT) AS "reactable_id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."created_at" - AS TEXT) AS "created_at", CAST(t."updated_at" AS TEXT) AS "updated_at" FROM "reactions" t - raw_repositories: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."url" - AS TEXT) AS "url", CAST(t."login" AS TEXT) AS "login", CAST(t."password" AS TEXT) AS "password", - CAST(t."root_url" AS TEXT) AS "root_url", CAST(t."type" AS TEXT) AS "type", CAST(t."path_encoding" - AS TEXT) AS "path_encoding", CAST(t."log_encoding" AS TEXT) AS "log_encoding", CAST(t."extra_info" - AS TEXT) AS "extra_info", CAST(t."identifier" AS TEXT) AS "identifier", CAST(t."is_default" AS - TEXT) AS "is_default", CAST(t."created_on" AS TEXT) AS "created_on" FROM "repositories" t - raw_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."assignable" AS TEXT) AS "assignable", CAST(t."builtin" AS TEXT) - AS "builtin", CAST(t."permissions" AS TEXT) AS "permissions", CAST(t."issues_visibility" AS TEXT) - AS "issues_visibility", CAST(t."users_visibility" AS TEXT) AS "users_visibility", CAST(t."time_entries_visibility" - AS TEXT) AS "time_entries_visibility", CAST(t."all_roles_managed" AS TEXT) AS "all_roles_managed", - CAST(t."settings" AS TEXT) AS "settings", CAST(t."default_time_entry_activity_id" AS TEXT) AS - "default_time_entry_activity_id" FROM "roles" t - raw_roles_managed_roles: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."role_id" AS TEXT) AS "role_id", CAST(t."managed_role_id" AS TEXT) AS "managed_role_id" - FROM "roles_managed_roles" t - raw_schema_migrations: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."version" AS TEXT) AS "id", CAST(t."version" AS TEXT) AS "version" FROM - "schema_migrations" t - raw_settings: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."value" - AS TEXT) AS "value", CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "settings" t - raw_time_entries: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", - CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" - AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", - CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id" FROM "time_entries" t - raw_tokens: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."action" - AS TEXT) AS "action", CAST(t."value" AS TEXT) AS "value", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on" FROM "tokens" t - raw_trackers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."name" AS TEXT) AS "name", CAST(t."position" - AS TEXT) AS "position", CAST(t."is_in_roadmap" AS TEXT) AS "is_in_roadmap", CAST(t."fields_bits" - AS TEXT) AS "fields_bits", CAST(t."default_status_id" AS TEXT) AS "default_status_id", CAST(t."description" - AS TEXT) AS "description" FROM "trackers" t + AS TEXT) AS "updated_by_id", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" u2 WHERE + CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" + AS TEXT) END FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM + "issues" a1 WHERE a1."id" = t."journalized_id") AS "main__project", (SELECT CAST(a1."subject" + AS TEXT) FROM "issues" a1 WHERE a1."id" = t."journalized_id") AS "main__subject", CASE WHEN CAST(CAST(t."private_notes" + AS integer) AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__private_notes", + CASE WHEN t."updated_by_id" IS NULL THEN '1' ELSE '0' END AS "pin__updated_by_id", CASE WHEN CAST(t."user_id" + AS numeric) = CAST('5' AS numeric) THEN '1' ELSE '0' END AS "pin__user_id" FROM "journals" t raw_user_preferences: kind: sql sql_driver: psycopg2 @@ -461,93 +57,6 @@ verifier: sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) AS "time_zone" FROM "user_preferences" t - raw_users: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."login" AS TEXT) AS "login", CAST(t."hashed_password" - AS TEXT) AS "hashed_password", CAST(t."firstname" AS TEXT) AS "firstname", CAST(t."lastname" AS - TEXT) AS "lastname", CAST(t."admin" AS TEXT) AS "admin", CAST(t."status" AS TEXT) AS "status", - CAST(t."last_login_on" AS TEXT) AS "last_login_on", CAST(t."language" AS TEXT) AS "language", - CAST(t."auth_source_id" AS TEXT) AS "auth_source_id", CAST(t."created_on" AS TEXT) AS "created_on", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."type" AS TEXT) AS "type", CAST(t."mail_notification" - AS TEXT) AS "mail_notification", CAST(t."salt" AS TEXT) AS "salt", CAST(t."must_change_passwd" - AS TEXT) AS "must_change_passwd", CAST(t."passwd_changed_on" AS TEXT) AS "passwd_changed_on", - CAST(t."twofa_scheme" AS TEXT) AS "twofa_scheme", CAST(t."twofa_totp_key" AS TEXT) AS "twofa_totp_key", - CAST(t."twofa_totp_last_used_at" AS TEXT) AS "twofa_totp_last_used_at", CAST(t."twofa_required" - AS TEXT) AS "twofa_required" FROM "users" t - raw_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."name" - AS TEXT) AS "name", CAST(t."description" AS TEXT) AS "description", CAST(t."effective_date" AS - TEXT) AS "effective_date", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS - TEXT) AS "updated_on", CAST(t."wiki_page_title" AS TEXT) AS "wiki_page_title", CAST(t."status" - AS TEXT) AS "status", CAST(t."sharing" AS TEXT) AS "sharing" FROM "versions" t - raw_watchers: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."watchable_type" AS TEXT) AS "watchable_type", - CAST(t."watchable_id" AS TEXT) AS "watchable_id", CAST(t."user_id" AS TEXT) AS "user_id" FROM - "watchers" t - raw_wiki_content_versions: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_content_id" AS TEXT) AS "wiki_content_id", - CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" AS TEXT) AS "author_id", encode(t."data", - 'hex') AS "data", CAST(t."compression" AS TEXT) AS "compression", CAST(t."comments" AS TEXT) AS - "comments", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" - FROM "wiki_content_versions" t - raw_wiki_contents: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."page_id" AS TEXT) AS "page_id", CAST(t."author_id" - AS TEXT) AS "author_id", CAST(t."text" AS TEXT) AS "text", CAST(t."comments" AS TEXT) AS "comments", - CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."version" AS TEXT) AS "version" FROM "wiki_contents" - t - raw_wiki_pages: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."protected" AS TEXT) - AS "protected", CAST(t."parent_id" AS TEXT) AS "parent_id" FROM "wiki_pages" t - raw_wiki_redirects: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."wiki_id" AS TEXT) AS "wiki_id", CAST(t."title" - AS TEXT) AS "title", CAST(t."redirects_to" AS TEXT) AS "redirects_to", CAST(t."created_on" AS - TEXT) AS "created_on", CAST(t."redirects_to_wiki_id" AS TEXT) AS "redirects_to_wiki_id" FROM "wiki_redirects" - t - raw_wikis: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."start_page" - AS TEXT) AS "start_page", CAST(t."status" AS TEXT) AS "status" FROM "wikis" t - raw_workflows: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."old_status_id" - AS TEXT) AS "old_status_id", CAST(t."new_status_id" AS TEXT) AS "new_status_id", CAST(t."role_id" - AS TEXT) AS "role_id", CAST(t."assignee" AS TEXT) AS "assignee", CAST(t."author" AS TEXT) AS "author", - CAST(t."type" AS TEXT) AS "type", CAST(t."field_name" AS TEXT) AS "field_name", CAST(t."rule" - AS TEXT) AS "rule" FROM "workflows" t effects: - kind: record_written probe: surface=record|the main record holds the task's values @@ -564,8 +73,6 @@ effects: match: main__project: param: project - main__status: - param: status main__subject: param: subject notes: @@ -580,14 +87,19 @@ effects: literal: '1' expected_count: 1 count_new_only: true +- kind: record_written + probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app + match: + id: + literal: '1' + expected_count: 1 frame: - ignore_fields: + target_fields: - lock_version - others - - updated_on - target_fields: - p__status - status_id + - updated_on versions: read: kind: sql @@ -602,3 +114,22 @@ frame: author_field: author self: - param: bot_user_id + database: + dialect: postgresql + read: + sql_driver: psycopg2 + sql_connect_args: *id001 + sql_password_env: REDMINE_RO_DB_PASSWORD + allow_tables: + - table: journal_details + - table: issues + mode: skip + reason: read in full, every column, by this contract's own surface + - table: journals + mode: skip + reason: read in full, every column, by this contract's own surface + views: + user_preferences: raw_user_preferences + settle_s: 1 + settle_attempts: 5 + recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/report.json b/packages/flow/benchmark/generate_contracts/results/redmine/report.json index fc31d7963c..295e7829de 100644 --- a/packages/flow/benchmark/generate_contracts/results/redmine/report.json +++ b/packages/flow/benchmark/generate_contracts/results/redmine/report.json @@ -1,14 +1,22 @@ { "applied": [], + "read_account": [], "dialect": "postgresql", "tables": 58, "questions": [ { "number": 1, "key": "bookkeeping", - "text": "Is this the app's own bookkeeping, which a write may change without a task asking? Rows the app adds beside each write, expected exactly that many times (a second save adds a second one, which stops the write; list a table under logs_add to allow any number): custom_values (2 per write). Tables treated as the app's own logs (rows may be added, existing rows must not change; on PostgreSQL they are not read at all): journal_details Columns that change on their own and are not compared: issues: lock_version, updated_on; user_preferences: others Columns of a new record that differ between demonstrations and are not checked: time_entries: tmonth, tweek, tyear Answer `bookkeeping: accept`, or list changes under logs_add, logs_remove, columns_add, columns_remove.", + "text": "Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (issues: the target row): issues: lock_version, updated_on. Also changed with no task value, and not excused unless you add them under columns_add (a write that changes them stops): user_preferences: others. Rows the app adds beside each write, expected exactly that many times: custom_values (2 per write). Tables that gained a different number of rows in different demonstrations; they are not logs unless you list them under logs_add (rows may then be added, existing rows must not change): journal_details. Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): issues: created_on, description, updated_on; time_entries: created_on, tmonth, tweek, tyear, updated_on. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log.", "proposal": "accept", "answered": true + }, + { + "number": 2, + "key": "recheck_after_s", + "text": "How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `.", + "proposal": 300, + "answered": true } ], "kinds": { @@ -17,7 +25,7 @@ "main_table": "issues", "locations": { "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", - "customer_ref": "custom_values (whose customized_id is the issues key).value (phone)", + "customer_ref": "custom_values (whose customized_id is the issues key).value (exact)", "description": "issues.description (exact)", "due_date": "issues.due_date (date)", "environment": "custom_values (whose customized_id is the issues key).value (exact)", @@ -39,7 +47,26 @@ "dispatch_children": {}, "lists": {}, "side_rows": [], - "logs": [], + "logs": [ + "journal_details" + ], + "log_candidates": [], + "touched_rows": [ + { + "table": "user_preferences", + "selector": { + "id": { + "literal": "1" + } + }, + "columns": { + "others": "other" + }, + "row": "user_preferences row id=1", + "by_key": true + } + ], + "unexcused": {}, "refused": [], "unresolved": {}, "emitted": 26, @@ -83,6 +110,25 @@ "logs": [ "journal_details" ], + "log_candidates": [ + "journal_details" + ], + "touched_rows": [ + { + "table": "user_preferences", + "selector": { + "id": { + "literal": "1" + } + }, + "columns": { + "others": "other" + }, + "row": "user_preferences row id=1", + "by_key": true + } + ], + "unexcused": {}, "refused": [], "unresolved": {}, "emitted": 63, @@ -107,7 +153,26 @@ "dispatch_children": {}, "lists": {}, "side_rows": [], - "logs": [], + "logs": [ + "journal_details" + ], + "log_candidates": [], + "touched_rows": [ + { + "table": "user_preferences", + "selector": { + "id": { + "literal": "1" + } + }, + "columns": { + "others": "other" + }, + "row": "user_preferences row id=1", + "by_key": true + } + ], + "unexcused": {}, "refused": [], "unresolved": {}, "emitted": 4, @@ -137,7 +202,26 @@ "unless": null } ], - "logs": [], + "logs": [ + "journal_details" + ], + "log_candidates": [], + "touched_rows": [ + { + "table": "user_preferences", + "selector": { + "id": { + "literal": "1" + } + }, + "columns": { + "others": "other" + }, + "row": "user_preferences row id=1", + "by_key": true + } + ], + "unexcused": {}, "refused": [], "unresolved": {}, "emitted": 2, diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 7730d44f73..931bd9d147 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -203,8 +203,11 @@ times in every demonstration (an agenda event, a stream note, a price-history row) is declared with that count and its constant columns. A table whose added rows differ in number between demonstrations is listed in the bookkeeping question and is a log only if the person names it (`logs_add`); -otherwise every added row stops the write. The generator never decides a -table is a log from its name. +otherwise every added row stops the write. The person may also name a +counted table (an access log the app writes once per request: the agent may +make more requests than the demonstration did). A named table is a log for +every kind: rows may be added to it, and existing rows must not change. The +generator never decides a table is a log from its name. **Changes the app makes to existing rows.** A column of an existing row that the demonstrations changed with no task value is excused only on that row: @@ -231,9 +234,11 @@ takes the versions from the app's own audit trail when every update demonstration added a row there that names the record and the acting user (Redmine's journals, EspoCRM's `action_history_record`, Dolibarr's agenda events). The read is narrowed to the record's type and to write events: a -column whose value in the update rows differs from its value in the rows -creates added to the same table (EspoCRM's `action`, Dolibarr's `code`), so -another user only viewing the record doesn't count. Otherwise it uses a +text column, not an id, that holds one code of letters and underscores in +every update row and in none of the rows creates added to the same table +(EspoCRM's `action = update`, Dolibarr's `code = AC_COMPANY_MODIFY`), so +another user only viewing the record doesn't count. A time stamp or a token +the demonstrations happened to share is never used. Otherwise it uses a whole-number column named as a version that went up by one in every demonstration (Redmine's `lock_version`). With neither, the update has no version check, and `report.json` shows `versions: null`. If the app stops From d075d293bc20ae5cccf5be55415e2ed773ccdfe0 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 23:05:40 -0400 Subject: [PATCH 86/94] Move the generator's study evidence to the private study repository --- .../benchmark/generate_contracts/RESULTS.md | 119 - .../generate_contracts/demo_dolibarr.py | 153 - .../generate_contracts/demo_espocrm.py | 144 - .../benchmark/generate_contracts/demo_lib.py | 123 - .../generate_contracts/demo_redmine.py | 164 -- .../results/dolibarr/QUESTIONS.md | 24 - .../results/dolibarr/answers.yaml | 5 - ...l+name+phone+town+zip.8386ba67.create.yaml | 347 --- ...il+name+phone+town+zip.8386ba67.found.yaml | 228 -- ...ice_date+town+linesx2.17de9df9.create.yaml | 460 --- ...oice_date+town+linesx2.17de9df9.found.yaml | 389 --- ...excl_vat+ref+vat_rate.bcd72fd0.create.yaml | 528 ---- ..._excl_vat+ref+vat_rate.bcd72fd0.found.yaml | 333 --- ...w_email+new_phone+town.110f7554.found.yaml | 108 - ..._email+new_phone+town.110f7554.update.yaml | 230 -- .../results/dolibarr/manifest.yaml | 936 ------- .../results/dolibarr/report.json | 203 -- .../results/espocrm/QUESTIONS.md | 20 - .../results/espocrm/answers.yaml | 4 - ...last_name+phone+title.fef035cb.create.yaml | 400 --- ...+last_name+phone+title.fef035cb.found.yaml | 263 -- ...e+currency+name+stage.1d2f2376.create.yaml | 171 -- ...te+currency+name+stage.1d2f2376.found.yaml | 84 - ...on+duration_minutes+s.dca67171.create.yaml | 210 -- ...ion+duration_minutes+s.dca67171.found.yaml | 150 - ...t_name+last_name+value.a43b40a4.found.yaml | 115 - ..._name+last_name+value.a43b40a4.update.yaml | 157 -- .../results/espocrm/manifest.yaml | 2457 ----------------- .../results/espocrm/report.json | 278 -- .../results/redmine/QUESTIONS.md | 16 - .../results/redmine/answers.yaml | 4 - ...oject+subject+tracker.d268453f.create.yaml | 204 -- ...roject+subject+tracker.d268453f.found.yaml | 196 -- ...ject+project+spent_on.a2ffb74d.create.yaml | 92 - ...bject+project+spent_on.a2ffb74d.found.yaml | 87 - ...project+status+subject.4aa4d777.found.yaml | 126 - ...roject+status+subject.4aa4d777.update.yaml | 135 - .../results/redmine/manifest.yaml | 1052 ------- .../results/redmine/report.json | 231 -- .../benchmark/generate_contracts/summarize.py | 82 - .../benchmark/generate_contracts/validate.py | 158 -- 41 files changed, 11186 deletions(-) delete mode 100644 packages/flow/benchmark/generate_contracts/RESULTS.md delete mode 100644 packages/flow/benchmark/generate_contracts/demo_dolibarr.py delete mode 100644 packages/flow/benchmark/generate_contracts/demo_espocrm.py delete mode 100644 packages/flow/benchmark/generate_contracts/demo_lib.py delete mode 100644 packages/flow/benchmark/generate_contracts/demo_redmine.py delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/dolibarr/report.json delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/espocrm/report.json delete mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md delete mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml delete mode 100644 packages/flow/benchmark/generate_contracts/results/redmine/report.json delete mode 100644 packages/flow/benchmark/generate_contracts/summarize.py delete mode 100644 packages/flow/benchmark/generate_contracts/validate.py diff --git a/packages/flow/benchmark/generate_contracts/RESULTS.md b/packages/flow/benchmark/generate_contracts/RESULTS.md deleted file mode 100644 index e6391c26ce..0000000000 --- a/packages/flow/benchmark/generate_contracts/RESULTS.md +++ /dev/null @@ -1,119 +0,0 @@ -# Generated contracts on the first study's apps - -Written 2026-10-30. The generator (`python -m openadapt_flow.generate.verify_contract`) -was run on three of the first-contact study's apps, each on its own copy: -Dolibarr instance 1, Redmine instance 1 and EspoCRM instance 2. Each run used -only what a new customer has: - -- the app's read-only database account (the fixture's `read_access()`); -- the kinds and parameter names, and the example values, from the app's - `setup_view.yaml`; -- one demonstration write per example, made here through the app's public - REST API as the bot (`demo_.py`). An example whose record already - existed was replaced by a create with values of our own. - -The study's tasks, oracle and fault scripts were used only afterwards, by -`validate.py`, to judge the contracts: each task's reference correct write -and each planted fault, through `choose`, `begin_write` and `verify_write`. -Late-write faults also ran the recheck the contract declares. - -## Results - -| | Dolibarr | Redmine | EspoCRM | -| --- | --- | --- | --- | -| Database | MariaDB 11.4 | PostgreSQL | MariaDB | -| Questions for a person | **4** | **2** | **3** | -| First study, person decisions | 11 | 11 | 11 | -| Field sets with a contract pair | 93 | 95 | 235 | -| Correct writes confirmed (tasks and controls) | 40/43 | 32/38 | 27/40 | -| In-scope planted faults stopped | **22/22** | **19/19** | **21/21** | -| First study, in-scope faults stopped | 21/21 | 26/26 | 16/19 | -| Out-of-scope late writes stopped | 2/2 (recheck) | 1/2 (recheck) | 1/2 (recheck) | -| Lost-update versions (`frame.versions`) | agenda events (`llx_actioncomm`, `code = AC_COMPANY_MODIFY`) | journals | `action_history_record` (`action = update`, `target_type = Lead`) | -| `propose` time | 2 min | 0.5 min | 1 min | - -Every contract uses the whole-database frame (`frame.database`) with the -store's dialect, on PostgreSQL too, and declares the recheck window. - -The first study measured false stops on agents' writes; here the correct -writes are the pack's reference writes and the controls, so the two false -stop rates are not the same measure. - -### Questions asked - -- **Dolibarr** (4): which parameters are phone numbers (both proposed, - both confirmed); bookkeeping (agenda events per write, `tms`, - `fk_user_modif`, invoice totals and draft reference not checked; the - person added the `socialnetworks` rewrite under `columns_add`); the recheck - window (300 s); `invoice_date` is in `datef` and in `date_lim_reglement` - (picked `datef`). -- **Redmine** (2): bookkeeping (custom field rows per write, `updated_on`, - `lock_version`, the time entry's `tyear`, `tmonth`, `tweek`; the person - named `journal_details` a log and added `user_preferences.others`); the - recheck window (300 s). -- **EspoCRM** (3): which parameters are phone numbers (confirmed); - bookkeeping (`stream_updated_at`, `modified_at`, `call_user` and stream - note rows; the person named `action_history_record` and `auth_log_record` - logs, and left `note` and `note_user` out, so stream notes are counted); - the recheck window (300 s). - -The generator decides no log by table name. Every log was named by the -person in the bookkeeping question. - -### Setup steps the generator applied - -- Dolibarr and EspoCRM: `GRANT TRIGGER ON .* TO ` - (`--grant-trigger`): the whole-database frame must see triggers. -- Redmine: none. The read account needs `SELECT` on the sequences too, - which the study's fixture grants. - -### False stops, by cause - -| App | Case | Cause | -| --- | --- | --- | -| Dolibarr | F18 (control) | Another user's edit to an unrelated customer: not attributed | -| Dolibarr | F21 (control) | Two saves add two agenda events; the contract expects one per write | -| Dolibarr | F30 (control) | A resave of a value already in place adds an agenda event; the `found` contract allows no change | -| Redmine | T19 | Closing an issue sets `closed_on`; no demonstration closed one | -| Redmine | T22, T23, T28 | The issue subject names issues in two projects: a display value must name exactly one row | -| Redmine | F21 (control) | Another user's edit to an unrelated issue: not attributed | -| Redmine | F24 (control) | Two saves add two journals | -| EspoCRM | T19, T23 | A status change adds a stream note; no demonstration changed the status | -| EspoCRM | T21 | The reference write removes the old email address row when replacing it; the demonstration didn't | -| EspoCRM | T22 | `field: phone` has no contract: no demonstration, and phones live in another table | -| EspoCRM | T29 to T34, T36 | Logging a call adds stream notes for each linked record and touches the contact's `stream_updated_at`, in numbers the demonstrations didn't show. `note` isn't a log unless the person says so | -| EspoCRM | F22, F27 (controls) | Another user's unrelated edit or new record: not attributed | - -The stream-note stops are the price of not calling `note` a log: a person -who names it adds those rows to the logs, and an extra stream note (a -planted fault) would then pass. - -### Misses - -Out of scope only: Redmine F20 and EspoCRM F21. Both are late writes to the -main record's table, which the contract reads through its own surface; the -recheck reads the rest of the database again but not that surface. No -in-scope planted fault was confirmed on any app. - -## Reproduce - -```bash -cd packages/flow/benchmark/generate_contracts -FCG_INSTANCE=1 FCG_DOLIBARR_READER_PASSWORD=... python demo_dolibarr.py WORK -python -m openadapt_flow.generate.verify_contract propose --read WORK/read.yaml \ - --kinds WORK/kinds.yaml --demos WORK/demos --out WORK/out \ - --actor-param bot_user_id=2 --answers WORK/answers.yaml \ - --grant-trigger --admin-read admin.yaml --reader-account "'fcg_reader'@'%'" -FCG_INSTANCE=1 python validate.py dolibarr WORK --actor-id 2 -python summarize.py dolibarr WORK -``` - -`results//` holds each run's `QUESTIONS.md`, `answers.yaml`, -`report.json`, `manifest.yaml` and two example contract pairs per app (the -full set is regenerated by the commands above). The fault scripts and the -oracle stay in the study's private pack. - -Code: the final runs used this branch rebased on `main` (with #1661 to -#1669 and #1673): Redmine and EspoCRM at `0bd47c616`, Dolibarr at -`f4cb759f9`. The commit after narrows the versions filter to letter-only -codes; the three apps' versions reads are the same under it. diff --git a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py b/packages/flow/benchmark/generate_contracts/demo_dolibarr.py deleted file mode 100644 index 85bcc4a74a..0000000000 --- a/packages/flow/benchmark/generate_contracts/demo_dolibarr.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Dolibarr (first study, instance 1): demonstrations, then the generator. - - FCG_INSTANCE=1 python benchmark/generate_contracts/demo_dolibarr.py WORKDIR - -Makes one demonstration write per example in ``setup_view.yaml`` through -Dolibarr's REST API as the bot (an example whose record already exists is -skipped: the right outcome is no write), then runs ``propose``. -""" - -from __future__ import annotations - -import calendar -import sys -from pathlib import Path - -import pymysql -import requests -from demo_lib import demo, fixture, propose, setup_view, write_inputs - -APP = "dolibarr" - - -def main(work: Path) -> None: - fx = fixture(APP) - fx.reset() - bot = fx.bot() - ro = fx.read_access() - read = { - "sql_driver": "pymysql", - "sql_connect_args": { - "host": ro["host"], - "port": ro["port"], - "user": ro["user"], - "database": ro["database"], - }, - "sql_password_env": "FCG_DOLIBARR_READER_PASSWORD", - } - write_inputs(work, APP, read) - db = pymysql.connect( - host=ro["host"], - port=ro["port"], - user=ro["user"], - password=ro["password"], - database=ro["database"], - autocommit=True, - ) - - def q(sql: str, args: tuple = ()) -> list[tuple]: - with db.cursor() as cur: - cur.execute(sql, args) - return list(cur.fetchall()) - - def api(method: str, path: str, payload: object = None) -> object: - r = requests.request( - method, - bot["base_url"] + path, - headers=bot["headers"], - json=payload, - timeout=60, - ) - r.raise_for_status() - return r.json() - - def customer(name: str, town: str) -> int | None: - db.commit() - rows = q( - "SELECT rowid FROM llx_societe WHERE BINARY nom=%s AND BINARY town=%s", - (name, town), - ) - return int(rows[0][0]) if len(rows) == 1 else None - - def country(label: str) -> int: - return int(q("SELECT rowid FROM llx_c_country WHERE label=%s", (label,))[0][0]) - - def product(ref: str) -> tuple[int, str, str]: - db.commit() - rid, price, tva = q( - "SELECT rowid, price, tva_tx FROM llx_product WHERE BINARY ref=%s", (ref,) - )[0] - return int(rid), str(price), str(tva) - - for k in setup_view(APP)["kinds"]: - for ex in k["examples"]: - v = ex["values"] - kind = k["kind"] - if kind == "create_customer": - if customer(v["name"], v["town"]) is not None: - # Already there (a "make sure" example): demonstrate a - # second create with values of our own instead. - v = { - **v, - "name": "Atelier Démo Générateur", - "address": "8 place du Marché", - "zip": "21000", - "town": "Dijon", - "phone": "03 80 30 31 32", - "email": "atelier@demo-generateur.example", - } - payload = {"name": v["name"], "address": v["address"], "zip": v["zip"], "town": v["town"], - "country_id": country(v["country"]), "phone": v["phone"], "email": v["email"], "client": 1} # fmt: skip - demo(work, kind, v, lambda p=payload: api("POST", "/thirdparties", p)) - elif kind == "create_product": - db.commit() - if q("SELECT 1 FROM llx_product WHERE BINARY ref=%s", (v["ref"],)): - v = { - **v, - "ref": "GEN-DEMO-2", - "label": "Coffret démo générateur", - "price_excl_vat": "12.40", - "vat_rate": "20", - } - payload = {"ref": v["ref"], "label": v["label"], "type": 0, "price": v["price_excl_vat"], - "price_base_type": "HT", "tva_tx": v["vat_rate"], "status": 1, "status_buy": 1} # fmt: skip - demo(work, kind, v, lambda p=payload: api("POST", "/products", p)) - elif kind == "create_draft_invoice": - y, m, d = (int(x) for x in v["invoice_date"].split("-")) - lines = [] - for ln in v["lines"]: - pid, price, tva = product(ln["product_ref"]) - lines.append( - { - "fk_product": pid, - "qty": ln["qty"], - "subprice": price, - "tva_tx": tva, - "product_type": 0, - } - ) - payload = {"socid": customer(v["customer"], v["town"]), "type": 0, - "date": calendar.timegm((y, m, d, 12, 0, 0, 0, 0, 0)), "lines": lines} # fmt: skip - demo(work, kind, v, lambda p=payload: api("POST", "/invoices", p)) - elif kind == "update_customer": - cid = customer(v["customer"], v["town"]) - change = { - key[4:]: x - for key, x in v.items() - if key.startswith("new_") and x is not None - } - demo( - work, - kind, - v, - lambda c=cid, p=change: api("PUT", f"/thirdparties/{c}", p), - ) - bot_id = q("SELECT rowid FROM llx_user WHERE login='fcg_bot'")[0][0] - report = propose(work, {"bot_user_id": str(bot_id)}) - print("questions:", len(report["questions"])) - db.close() # an open read transaction would block the reset - fx.reset() - - -if __name__ == "__main__": - main(Path(sys.argv[1])) diff --git a/packages/flow/benchmark/generate_contracts/demo_espocrm.py b/packages/flow/benchmark/generate_contracts/demo_espocrm.py deleted file mode 100644 index 84feac8369..0000000000 --- a/packages/flow/benchmark/generate_contracts/demo_espocrm.py +++ /dev/null @@ -1,144 +0,0 @@ -"""EspoCRM (first study, instance 2): demonstrations, then the generator. - - FCG_INSTANCE=2 python benchmark/generate_contracts/demo_espocrm.py WORKDIR - -One demonstration write per example in ``setup_view.yaml``, through -EspoCRM's REST API as the API user ``crm-bot``. Record ids for names are -looked up through the read-only account (exact, byte for byte: the name -columns use a case- and accent-insensitive collation). -""" - -from __future__ import annotations - -import datetime as dt -import sys -from pathlib import Path - -import pymysql -import requests -from demo_lib import demo, fixture, propose, setup_view, write_inputs - -APP = "espocrm" - - -def main(work: Path, extra: list[str]) -> None: - fx = fixture(APP) - fx.reset() - bot = fx.bot() - ro = fx.read_access() - read = { - "sql_driver": "pymysql", - "sql_connect_args": {"host": ro["host"], "port": ro["port"], "user": ro["user"], - "database": ro["database"], "charset": "utf8mb4"}, - "sql_password_env": "ESPOCRM_DB_RO_PASSWORD", - } # fmt: skip - write_inputs(work, APP, read) - db = pymysql.connect(host=ro["host"], port=ro["port"], user=ro["user"], password=ro["password"], - database=ro["database"], charset="utf8mb4", autocommit=True) # fmt: skip - - def one(sql: str, args: tuple = ()) -> str: - with db.cursor() as cur: - cur.execute(sql, args) - rows = cur.fetchall() - if len(rows) != 1: - raise RuntimeError(f"{sql} {args}: {len(rows)} rows") - return str(rows[0][0]) - - def account(name: str, city: str) -> str: - return one( - "SELECT id FROM account WHERE BINARY name=%s AND BINARY billing_address_city=%s AND deleted=0", - (name, city), - ) - - def person(table: str, full: str, company: str | None = None) -> str: - first, last = full.split(" ", 1) - sql = f"SELECT id FROM {table} WHERE BINARY first_name=%s AND BINARY last_name=%s AND deleted=0" - args: tuple = (first, last) - if company is not None: - sql += " AND BINARY account_name=%s" - args += (company,) - return one(sql, args) - - def api(method: str, path: str, payload: dict) -> None: - r = requests.request( - method, - bot["base_url"] + path, - headers=bot["headers"], - json=payload, - timeout=60, - ) - r.raise_for_status() - - bot_id = one("SELECT id FROM user WHERE user_name=%s", ("crm-bot",)) - for k in setup_view(APP)["kinds"]: - kind = k["kind"] - for ex in k["examples"]: - v = ex["values"] - if kind == "create_contact": - with db.cursor() as cur: - cur.execute("SELECT COUNT(*) FROM contact WHERE BINARY first_name=%s AND BINARY last_name=%s AND deleted=0", - (v["first_name"], v["last_name"])) # fmt: skip - if cur.fetchone()[0]: - # Already there: a create with values of our own. - v = {**v, "first_name": "Lena", "last_name": "Okafor", - "email": "lena.okafor@aurora-logistics.example"} - body = {"firstName": v["first_name"], "lastName": v["last_name"], - "accountId": account(v["account"], v["account_city"])} # fmt: skip - if v.get("title"): - body["title"] = v["title"] - if v.get("email"): - body["emailAddress"] = v["email"] - if v.get("phone"): - body["phoneNumber"] = v["phone"] - demo(work, kind, v, lambda b=body: api("POST", "/Contact", b)) - elif kind == "create_opportunity": - with db.cursor() as cur: - cur.execute( - "SELECT COUNT(*) FROM opportunity WHERE BINARY name=%s AND deleted=0", - (v["name"],), - ) - if cur.fetchone()[0]: - v = {**v, "name": "Aurora – Fleet Telematics Trial", "account": "Aurora Logistics", - "account_city": "Boston", "amount": 12500, "stage": "Proposal", - "close_date": "2027-02-15"} - body = {"name": v["name"], "accountId": account(v["account"], v["account_city"]), - "amount": v["amount"], "amountCurrency": v["currency"], "stage": v["stage"], - "closeDate": v["close_date"]} # fmt: skip - demo(work, kind, v, lambda b=body: api("POST", "/Opportunity", b)) - elif kind == "update_lead": - lid = one("SELECT id FROM lead WHERE BINARY first_name=%s AND BINARY last_name=%s " - "AND BINARY account_name=%s AND deleted=0", - (v["first_name"], v["last_name"], v["company"])) # fmt: skip - if v["field"] == "email": - body = {"emailAddressData": [{"emailAddress": v["value"], "primary": True, - "optOut": False, "invalid": False}]} # fmt: skip - else: - body = {v["field"]: v["value"]} - demo(work, kind, v, lambda i=lid, b=body: api("PUT", f"/Lead/{i}", b)) - elif kind == "log_call": - start = dt.datetime.strptime(v["date_start"], "%Y-%m-%d %H:%M") - end = start + dt.timedelta(minutes=int(v["duration_minutes"])) - body = {"name": v["subject"], "status": v["status"], "direction": v["direction"], - "dateStart": start.strftime("%Y-%m-%d %H:%M:%S"), - "dateEnd": end.strftime("%Y-%m-%d %H:%M:%S"), "assignedUserId": bot_id} # fmt: skip - if v.get("account"): - body.update( - parentType="Account", - parentId=account(v["account"], v["account_city"]), - ) - if v.get("lead"): - lead = person("lead", v["lead"], v.get("lead_company")) - body.update(parentType="Lead", parentId=lead, leadsIds=[lead]) - if v.get("contact"): - body["contactsIds"] = [person("contact", v["contact"])] - if v.get("description"): - body["description"] = v["description"] - demo(work, kind, v, lambda b=body: api("POST", "/Call", b)) - report = propose(work, {"bot_user_id": bot_id}, extra) - print("questions:", len(report["questions"])) - db.close() - fx.reset() - - -if __name__ == "__main__": - main(Path(sys.argv[1]), sys.argv[2:]) diff --git a/packages/flow/benchmark/generate_contracts/demo_lib.py b/packages/flow/benchmark/generate_contracts/demo_lib.py deleted file mode 100644 index 40499cc211..0000000000 --- a/packages/flow/benchmark/generate_contracts/demo_lib.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Shared steps for running the contract generator on a first-study app. - -The generator is run the way a new customer would run it: it gets only the -app's read-only database account, the kinds and parameter names from the -app's ``setup_view.yaml``, and demonstration writes made here through the -app's public REST API (one per example). Nothing from the study's tasks, -oracle or faults reaches the generator. ``validate.py`` uses those later, -only to judge the contracts it wrote. - -Paths: ``FCG`` is the first study's folder (``apps//fixture.py``). -""" - -from __future__ import annotations - -import importlib.util -import json -import os -import sys -from pathlib import Path -from typing import Any, Callable - -import yaml - -from openadapt_flow.generate import verify_contract as gen - -FCG = Path( - os.environ.get( - "FCG_DIR", - str( - Path.home() - / "oa/worktrees/fcg-retest/evidence/first-contact-generalization-2026-10" - ), - ) -) - - -def load(name: str, path: Path) -> Any: - spec = importlib.util.spec_from_file_location(name, path) - mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type] - sys.modules[name] = mod - spec.loader.exec_module(mod) # type: ignore[union-attr] - return mod - - -def fixture(app: str) -> Any: - sys.path.insert(0, str(FCG / "apps" / app)) - return load(f"fcg_fixture_{app}", FCG / "apps" / app / "fixture.py") - - -def setup_view(app: str) -> dict[str, Any]: - return yaml.safe_load( - (FCG / "apps" / app / "setup_view.yaml").read_text(encoding="utf-8") - ) - - -def write_inputs(work: Path, app: str, read: dict[str, Any]) -> list[str]: - """``read.yaml`` and ``kinds.yaml`` in ``work``; the CLI's read flags.""" - - work.mkdir(parents=True, exist_ok=True) - (work / "read.yaml").write_text(yaml.safe_dump(read), encoding="utf-8") - view = setup_view(app) - kinds = { - "kinds": [ - { - "kind": k["kind"], - "description": k.get("description", ""), - "parameters": k["parameters"], - "examples": [{"values": e["values"]} for e in k.get("examples", [])], - } - for k in view["kinds"] - ] - } - (work / "kinds.yaml").write_text( - yaml.safe_dump(kinds, allow_unicode=True), encoding="utf-8" - ) - return ["--read", str(work / "read.yaml")] - - -def demo( - work: Path, kind: str, values: dict[str, Any], write: Callable[[], Any] -) -> None: - """Snapshot, make one demonstration write, record the diff.""" - - flags = ["--read", str(work / "read.yaml")] - assert gen.main(["snapshot", *flags, "--out", str(work / "before.json")]) == 0 - write() - assert ( - gen.main( - [ - "record", - *flags, - "--before", - str(work / "before.json"), - "--kind", - kind, - "--values", - json.dumps(values, ensure_ascii=False), - "--demos", - str(work / "demos"), - ] - ) - == 0 - ) - - -def propose(work: Path, actor: dict[str, str], extra: list[str] = ()) -> dict[str, Any]: - argv = [ - "propose", - "--read", - str(work / "read.yaml"), - "--kinds", - str(work / "kinds.yaml"), - "--demos", - str(work / "demos"), - "--out", - str(work / "out"), - *[f"--actor-param={k}={v}" for k, v in actor.items()], - *extra, - ] - if (work / "answers.yaml").exists(): - argv += ["--answers", str(work / "answers.yaml")] - gen.main(argv) - return json.loads((work / "out" / "report.json").read_text(encoding="utf-8")) diff --git a/packages/flow/benchmark/generate_contracts/demo_redmine.py b/packages/flow/benchmark/generate_contracts/demo_redmine.py deleted file mode 100644 index e98b53298a..0000000000 --- a/packages/flow/benchmark/generate_contracts/demo_redmine.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Redmine (first study, instance 1): demonstrations, then the generator. - - FCG_INSTANCE=1 python benchmark/generate_contracts/demo_redmine.py WORKDIR - -One demonstration write per example in ``setup_view.yaml``, through -Redmine's REST API as the bot. Ids for names (project, tracker, user) are -looked up through the read-only account, as a setup agent would. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import psycopg2 -import requests -from demo_lib import demo, fixture, propose, setup_view, write_inputs - -APP = "redmine" - - -def main(work: Path) -> None: - fx = fixture(APP) - fx.reset() - bot = fx.bot() - ro = fx.read_access() - read = { - "sql_driver": "psycopg2", - "sql_connect_args": { - "host": ro["host"], - "port": ro["port"], - "user": ro["user"], - "dbname": ro["database"], - }, - "sql_password_env": "REDMINE_RO_DB_PASSWORD", - } - write_inputs(work, APP, read) - db = psycopg2.connect(ro["dsn"]) - db.autocommit = True - - def one(sql: str, args: tuple = ()) -> object: - with db.cursor() as cur: - cur.execute(sql, args) - rows = cur.fetchall() - if len(rows) != 1: - raise RuntimeError(f"{sql} {args}: {len(rows)} rows") - return rows[0][0] - - def project(name: str) -> int: - return int(one("SELECT id FROM projects WHERE name=%s", (name,))) - - def user(name: str) -> int: - return int( - one("SELECT id FROM users WHERE firstname || ' ' || lastname = %s", (name,)) - ) - - def enum(kind: str, name: str) -> int: - return int( - one("SELECT id FROM enumerations WHERE type=%s AND name=%s", (kind, name)) - ) - - def issue(proj: str, subject: str) -> int: - return int(one("SELECT i.id FROM issues i JOIN projects p ON p.id=i.project_id " - "WHERE p.name=%s AND i.subject=%s", (proj, subject))) # fmt: skip - - def field(name: str) -> int: - return int(one("SELECT id FROM custom_fields WHERE name=%s", (name,))) - - def api(method: str, path: str, payload: object) -> None: - r = requests.request( - method, - bot["base_url"] + path, - headers=bot["headers"], - json=payload, - timeout=60, - ) - r.raise_for_status() - - def create_payload(v: dict) -> dict: - body: dict = { - "project_id": project(v["project"]), - "tracker_id": int( - one("SELECT id FROM trackers WHERE name=%s", (v["tracker"],)) - ), - "subject": v["subject"], - "priority_id": enum("IssuePriority", v["priority"]), - } - if v.get("assignee"): - body["assigned_to_id"] = user(v["assignee"]) - if v.get("due_date"): - body["due_date"] = v["due_date"] - if v.get("description"): - body["description"] = v["description"] - cf = [ - {"id": field(n), "value": v[k]} - for k, n in ( - ("customer_ref", "Customer Ref"), - ("environment", "Environment"), - ) - if v.get(k) is not None - ] - if cf: - body["custom_fields"] = cf - return {"issue": body} - - for k in setup_view(APP)["kinds"]: - kind = k["kind"] - for ex in k["examples"]: - v = ex["values"] - if kind in ("create_issue", "ensure_issue"): - if kind == "ensure_issue" and int(one( - "SELECT COUNT(*) FROM issues i JOIN projects p ON p.id=i.project_id " - "WHERE p.name=%s AND i.subject=%s", (v["project"], v["subject"]))): # fmt: skip - # Already there: demonstrate a create with values of our own. - v = {**v, "subject": "Generator demo: rotate the backup keys", - "priority": "Low", "assignee": "Jon Park"} - demo( - work, - kind, - v, - lambda p=create_payload(v): api("POST", "/issues.json", p), - ) - elif kind == "update_issue": - body: dict = {} - if v.get("status"): - body["status_id"] = int( - one( - "SELECT id FROM issue_statuses WHERE name=%s", - (v["status"],), - ) - ) - if v.get("assignee"): - body["assigned_to_id"] = user(v["assignee"]) - if v.get("priority"): - body["priority_id"] = enum("IssuePriority", v["priority"]) - for key in ("due_date", "done_ratio", "notes"): - if v.get(key) is not None: - body[key] = v[key] - iid = issue(v["project"], v["subject"]) - demo( - work, - kind, - v, - lambda i=iid, b=body: api("PUT", f"/issues/{i}.json", {"issue": b}), - ) - elif kind == "log_time": - body = {"issue_id": issue(v["project"], v["issue_subject"]), "hours": v["hours"], - "spent_on": v["spent_on"], "activity_id": enum("TimeEntryActivity", v["activity"]), - "comments": v["comments"]} # fmt: skip - demo( - work, - kind, - v, - lambda b=body: api("POST", "/time_entries.json", {"time_entry": b}), - ) - bot_id = one("SELECT id FROM users WHERE login=%s", (bot["login"],)) - report = propose(work, {"bot_user_id": str(bot_id)}) - print("questions:", len(report["questions"])) - db.close() - fx.reset() - - -if __name__ == "__main__": - main(Path(sys.argv[1])) diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md deleted file mode 100644 index 8bf6405a80..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/QUESTIONS.md +++ /dev/null @@ -1,24 +0,0 @@ -# Questions for a person - -1. These values were stored as other text with the same digits (a phone number written with spaces stored without): create_customer.phone, update_customer.new_phone. Compare them as phone numbers (the same digits, at least 7, and a task value that isn't shaped like a phone number is compared exactly)? Answer `phones: [, ...]` with the ones that are phone numbers; any other is compared exactly. - - Answer key: `phones`. Proposed: `['create_customer.phone', 'update_customer.new_phone']`. - - Answered: `["create_customer.phone", "update_customer.new_phone"]` - -2. Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (llx_societe: the target row): llx_societe: fk_user_modif, tms. Also changed with no task value, and not excused unless you add them under columns_add (a write that changes them stops): llx_societe: socialnetworks. Rows the app adds beside each write, expected exactly that many times: llx_actioncomm (1 per write), llx_actioncomm_resources (1 per write). Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): llx_facture: close_code, close_note, date_lim_reglement, datec, extraparams, fk_currency, import_key, increment, last_main_doc, location_incoterms, model_pdf, module_source, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, note_private, note_public, payment_reference, pos_source, ref, ref_client, ref_ext, tms, total_ht, total_ttc, total_tva; llx_facturedet: batch, description, import_key, label, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref_ext, total_ht, total_ttc, total_tva, vat_src_code; llx_product: accountancy_code_buy, accountancy_code_buy_export, accountancy_code_buy_intra, accountancy_code_sell, accountancy_code_sell_export, accountancy_code_sell_intra, barcode, batch_mask, canvas, customcode, datec, default_vat_code, description, duration, import_key, last_main_doc, model_pdf, note, note_public, partnumber, price_label, price_ttc, ref_ext, tms, url; llx_product_price: date_price, default_vat_code, import_key, multicurrency_code, price_label, price_ttc, tms; llx_societe: accountancy_code_buy, accountancy_code_customer_general, accountancy_code_sell, accountancy_code_supplier_general, ape, barcode, canvas, code_client, code_compta, code_compta_fournisseur, code_fournisseur, datec, default_lang, deposit_percent, fax, fk_currency, fk_prospectlevel, georesultcode, idprof4, idprof5, idprof6, import_key, ip, last_main_doc, location_incoterms, logo, logo_squarred, model_pdf, multicurrency_code, name_alias, note_private, note_public, phone_mobile, prefix_comm, ref_ext, siren, siret, supplier_account, tms, tp_payment_reference, tva_intra, url, webservices_key, webservices_url. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log. - - Answer key: `bookkeeping`. Proposed: `accept`. - - Answered: `{"columns_add": {"llx_societe": ["socialnetworks"]}}` - -3. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. - - Answer key: `recheck_after_s`. Proposed: `300`. - - Answered: `300` - -4. Kind `create_draft_invoice`: `invoice_date` is in several places: [0] llx_facture.datef (date); [1] llx_facture.date_lim_reglement (date) Answer `locations.create_draft_invoice: {: }` to pick one. - - Answer key: `locations.create_draft_invoice`. Proposed: `None`. - - Answered: `{"invoice_date": 0}` - -## Kinds - -- `create_customer` (create, main table `llx_societe`): 64 contract(s). -- `create_product` (create, main table `llx_product`): 4 contract(s). -- `create_draft_invoice` (create, main table `llx_facture`): 10 contract(s). -- `update_customer` (update, main table `llx_societe`): 15 contract(s). diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml deleted file mode 100644 index 01984d2f3d..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/answers.yaml +++ /dev/null @@ -1,5 +0,0 @@ -phones: [create_customer.phone, update_customer.new_phone] -bookkeeping: - columns_add: {llx_societe: [socialnetworks]} -recheck_after_s: 300 -locations.create_draft_invoice: {invoice_date: 0} diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml deleted file mode 100644 index 93e782d621..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml +++ /dev/null @@ -1,347 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_customer, create, fields address, country, email, name, phone, town, zip. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` - AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` - AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, - CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) - AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, - CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS - CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, - CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) - AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS - `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) - AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS - `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` - AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, - CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, - CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, - CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` - AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` - AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS - `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, - CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, - CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, - CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` - AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` - AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` - AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` - AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` - AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` - AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` - AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` - AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, - CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` - AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` - AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` - AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` - AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` - AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` - AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` - AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` - AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) - AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) - AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` - AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, - CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS - CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key`, (SELECT CASE WHEN - (SELECT COUNT(*) FROM `llx_c_country` u1 WHERE CAST(CAST(u1.`label` AS CHAR) AS BINARY) = CAST(CAST(a1.`label` - AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`label` AS CHAR) END FROM `llx_c_country` a1 WHERE CAST(a1.`rowid` - AS BINARY) = CAST(t.`fk_pays` AS BINARY)) AS `p__country`, CASE WHEN t.`capital` IS NULL THEN - '1' ELSE '0' END AS `pin__capital`, CASE WHEN t.`client` = CAST('1' AS DECIMAL(65,30)) THEN '1' - ELSE '0' END AS `pin__client`, CASE WHEN t.`cond_reglement` IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement`, - CASE WHEN t.`cond_reglement_supplier` IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement_supplier`, - CASE WHEN t.`customer_bad` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_bad`, - CASE WHEN t.`customer_rate` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_rate`, - CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE - WHEN t.`fk_account` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_account`, - CASE WHEN t.`fk_barcode_type` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, - CASE WHEN t.`fk_departement` IS NULL THEN '1' ELSE '0' END AS `pin__fk_departement`, CASE WHEN - t.`fk_effectif` IS NULL THEN '1' ELSE '0' END AS `pin__fk_effectif`, CASE WHEN t.`fk_forme_juridique` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_forme_juridique`, CASE WHEN t.`fk_incoterms` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_incoterms`, CASE WHEN t.`fk_multicurrency` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_shipping_method` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_shipping_method`, CASE WHEN t.`fk_stcomm` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_stcomm`, CASE WHEN t.`fk_typent` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_typent`, CASE WHEN t.`fk_warehouse` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`fournisseur` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fournisseur`, CASE WHEN t.`geolat` IS NULL THEN '1' ELSE '0' END - AS `pin__geolat`, CASE WHEN t.`geolong` IS NULL THEN '1' ELSE '0' END AS `pin__geolong`, CASE - WHEN t.`geopoint` IS NULL THEN '1' ELSE '0' END AS `pin__geopoint`, CASE WHEN t.`localtax1_assuj` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_assuj`, CASE WHEN t.`localtax1_value` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_value`, CASE WHEN t.`localtax2_assuj` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_assuj`, CASE WHEN t.`localtax2_value` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_value`, CASE WHEN t.`mode_reglement` - IS NULL THEN '1' ELSE '0' END AS `pin__mode_reglement`, CASE WHEN t.`mode_reglement_supplier` - IS NULL THEN '1' ELSE '0' END AS `pin__mode_reglement_supplier`, CASE WHEN t.`order_min_amount` - IS NULL THEN '1' ELSE '0' END AS `pin__order_min_amount`, CASE WHEN t.`outstanding_limit` IS NULL - THEN '1' ELSE '0' END AS `pin__outstanding_limit`, CASE WHEN t.`parent` IS NULL THEN '1' ELSE - '0' END AS `pin__parent`, CASE WHEN t.`price_level` IS NULL THEN '1' ELSE '0' END AS `pin__price_level`, - CASE WHEN t.`remise_client` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_client`, - CASE WHEN t.`remise_supplier` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_supplier`, - CASE WHEN t.`status` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__status`, CASE - WHEN t.`statut` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__statut`, CASE WHEN - t.`supplier_order_min_amount` IS NULL THEN '1' ELSE '0' END AS `pin__supplier_order_min_amount`, - CASE WHEN t.`supplier_rate` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__supplier_rate`, - CASE WHEN t.`transport_mode` IS NULL THEN '1' ELSE '0' END AS `pin__transport_mode`, CASE WHEN - t.`transport_mode_supplier` IS NULL THEN '1' ELSE '0' END AS `pin__transport_mode_supplier`, CASE - WHEN t.`tva_assuj` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tva_assuj`, CASE - WHEN t.`vat_reverse_charge` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__vat_reverse_charge` - FROM `llx_societe` t - raw_llx_actioncomm: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` - AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, - CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` - AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` - AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` - AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS - `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, - CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, - CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` - AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) - AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, - CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` - AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` - AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` - AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS - CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) - AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, - CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` - AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS - `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` - AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` - AS CHAR) AS `extraparams` FROM `llx_actioncomm` t - raw_llx_actioncomm_resources: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` - AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` - AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` - AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` - t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - address: - param: address - p__country: - param: country - email: - param: email - kind: email - nom: - param: name - phone: - param: phone - kind: phone - town: - param: town - zip: - param: zip - pin__capital: - literal: '1' - pin__client: - literal: '1' - pin__cond_reglement: - literal: '1' - pin__cond_reglement_supplier: - literal: '1' - pin__customer_bad: - literal: '1' - pin__customer_rate: - literal: '1' - pin__entity: - literal: '1' - pin__fk_account: - literal: '1' - pin__fk_barcode_type: - literal: '1' - pin__fk_departement: - literal: '1' - pin__fk_effectif: - literal: '1' - pin__fk_forme_juridique: - literal: '1' - pin__fk_incoterms: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_shipping_method: - literal: '1' - pin__fk_stcomm: - literal: '1' - pin__fk_typent: - literal: '1' - fk_user_creat: - param: bot_user_id - fk_user_modif: - param: bot_user_id - pin__fk_warehouse: - literal: '1' - pin__fournisseur: - literal: '1' - pin__geolat: - literal: '1' - pin__geolong: - literal: '1' - pin__geopoint: - literal: '1' - pin__localtax1_assuj: - literal: '1' - pin__localtax1_value: - literal: '1' - pin__localtax2_assuj: - literal: '1' - pin__localtax2_value: - literal: '1' - pin__mode_reglement: - literal: '1' - pin__mode_reglement_supplier: - literal: '1' - pin__order_min_amount: - literal: '1' - pin__outstanding_limit: - literal: '1' - pin__parent: - literal: '1' - pin__price_level: - literal: '1' - pin__remise_client: - literal: '1' - pin__remise_supplier: - literal: '1' - socialnetworks: - literal: 'null' - pin__status: - literal: '1' - pin__statut: - literal: '1' - pin__supplier_order_min_amount: - literal: '1' - pin__supplier_rate: - literal: '1' - pin__transport_mode: - literal: '1' - pin__transport_mode_supplier: - literal: '1' - pin__tva_assuj: - literal: '1' - pin__vat_reverse_charge: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm - match: - entity: - literal: '1' - event_paid: - literal: '0' - fk_action: - literal: '40' - fk_parent: - literal: '0' - fk_user_action: - param: bot_user_id - fk_user_author: - param: bot_user_id - fulldayevent: - literal: '0' - percent: - literal: '-1' - priority: - literal: '0' - status: - literal: '0' - transparency: - literal: '0' - visibility: - literal: default - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources - match: - element_type: - literal: user - fk_element: - param: bot_user_id - mandatory: - literal: '0' - expected_count: 1 - count_new_only: true -frame: - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_societe - views: - llx_actioncomm: raw_llx_actioncomm - llx_actioncomm_resources: raw_llx_actioncomm_resources - settle_s: 1 - settle_attempts: 5 - new_row_fields: - llx_actioncomm: - - code - - datec - - datep - - datep2 - - durationp - - elementtype - - email_from - - email_msgid - - email_sender - - email_subject - - email_to - - email_tobcc - - email_tocc - - errors_to - - extraparams - - fk_soc - - id - - import_key - - ip - - label - - location - - note - - recurid - - recurrule - - ref - - ref_ext - - reply_to - - tms - llx_actioncomm_resources: - - answer_status - - fk_actioncomm - - rowid - - transparency - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml deleted file mode 100644 index 1e227bba96..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml +++ /dev/null @@ -1,228 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_customer, found, fields address, country, email, name, phone, town, zip. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` - AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` - AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, - CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) - AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, - CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS - CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, - CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) - AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS - `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) - AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS - `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` - AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, - CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, - CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, - CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` - AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` - AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS - `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, - CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, - CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, - CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` - AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` - AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` - AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` - AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` - AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` - AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` - AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` - AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, - CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` - AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` - AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` - AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` - AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` - AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` - AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` - AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` - AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) - AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) - AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` - AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, - CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS - CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key`, (SELECT CASE WHEN - (SELECT COUNT(*) FROM `llx_c_country` u1 WHERE CAST(CAST(u1.`label` AS CHAR) AS BINARY) = CAST(CAST(a1.`label` - AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`label` AS CHAR) END FROM `llx_c_country` a1 WHERE CAST(a1.`rowid` - AS BINARY) = CAST(t.`fk_pays` AS BINARY)) AS `p__country`, CASE WHEN t.`capital` IS NULL THEN - '1' ELSE '0' END AS `pin__capital`, CASE WHEN t.`client` = CAST('1' AS DECIMAL(65,30)) THEN '1' - ELSE '0' END AS `pin__client`, CASE WHEN t.`cond_reglement` IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement`, - CASE WHEN t.`cond_reglement_supplier` IS NULL THEN '1' ELSE '0' END AS `pin__cond_reglement_supplier`, - CASE WHEN t.`customer_bad` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_bad`, - CASE WHEN t.`customer_rate` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__customer_rate`, - CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE - WHEN t.`fk_account` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_account`, - CASE WHEN t.`fk_barcode_type` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, - CASE WHEN t.`fk_departement` IS NULL THEN '1' ELSE '0' END AS `pin__fk_departement`, CASE WHEN - t.`fk_effectif` IS NULL THEN '1' ELSE '0' END AS `pin__fk_effectif`, CASE WHEN t.`fk_forme_juridique` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_forme_juridique`, CASE WHEN t.`fk_incoterms` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_incoterms`, CASE WHEN t.`fk_multicurrency` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_shipping_method` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_shipping_method`, CASE WHEN t.`fk_stcomm` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_stcomm`, CASE WHEN t.`fk_typent` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_typent`, CASE WHEN t.`fk_warehouse` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`fournisseur` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fournisseur`, CASE WHEN t.`geolat` IS NULL THEN '1' ELSE '0' END - AS `pin__geolat`, CASE WHEN t.`geolong` IS NULL THEN '1' ELSE '0' END AS `pin__geolong`, CASE - WHEN t.`geopoint` IS NULL THEN '1' ELSE '0' END AS `pin__geopoint`, CASE WHEN t.`localtax1_assuj` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_assuj`, CASE WHEN t.`localtax1_value` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_value`, CASE WHEN t.`localtax2_assuj` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_assuj`, CASE WHEN t.`localtax2_value` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_value`, CASE WHEN t.`mode_reglement` - IS NULL THEN '1' ELSE '0' END AS `pin__mode_reglement`, CASE WHEN t.`mode_reglement_supplier` - IS NULL THEN '1' ELSE '0' END AS `pin__mode_reglement_supplier`, CASE WHEN t.`order_min_amount` - IS NULL THEN '1' ELSE '0' END AS `pin__order_min_amount`, CASE WHEN t.`outstanding_limit` IS NULL - THEN '1' ELSE '0' END AS `pin__outstanding_limit`, CASE WHEN t.`parent` IS NULL THEN '1' ELSE - '0' END AS `pin__parent`, CASE WHEN t.`price_level` IS NULL THEN '1' ELSE '0' END AS `pin__price_level`, - CASE WHEN t.`remise_client` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_client`, - CASE WHEN t.`remise_supplier` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_supplier`, - CASE WHEN t.`status` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__status`, CASE - WHEN t.`statut` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__statut`, CASE WHEN - t.`supplier_order_min_amount` IS NULL THEN '1' ELSE '0' END AS `pin__supplier_order_min_amount`, - CASE WHEN t.`supplier_rate` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__supplier_rate`, - CASE WHEN t.`transport_mode` IS NULL THEN '1' ELSE '0' END AS `pin__transport_mode`, CASE WHEN - t.`transport_mode_supplier` IS NULL THEN '1' ELSE '0' END AS `pin__transport_mode_supplier`, CASE - WHEN t.`tva_assuj` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tva_assuj`, CASE - WHEN t.`vat_reverse_charge` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__vat_reverse_charge` - FROM `llx_societe` t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - address: - param: address - p__country: - param: country - email: - param: email - kind: email - nom: - param: name - phone: - param: phone - kind: phone - town: - param: town - zip: - param: zip - pin__capital: - literal: '1' - pin__client: - literal: '1' - pin__cond_reglement: - literal: '1' - pin__cond_reglement_supplier: - literal: '1' - pin__customer_bad: - literal: '1' - pin__customer_rate: - literal: '1' - pin__entity: - literal: '1' - pin__fk_account: - literal: '1' - pin__fk_barcode_type: - literal: '1' - pin__fk_departement: - literal: '1' - pin__fk_effectif: - literal: '1' - pin__fk_forme_juridique: - literal: '1' - pin__fk_incoterms: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_shipping_method: - literal: '1' - pin__fk_stcomm: - literal: '1' - pin__fk_typent: - literal: '1' - pin__fk_warehouse: - literal: '1' - pin__fournisseur: - literal: '1' - pin__geolat: - literal: '1' - pin__geolong: - literal: '1' - pin__geopoint: - literal: '1' - pin__localtax1_assuj: - literal: '1' - pin__localtax1_value: - literal: '1' - pin__localtax2_assuj: - literal: '1' - pin__localtax2_value: - literal: '1' - pin__mode_reglement: - literal: '1' - pin__mode_reglement_supplier: - literal: '1' - pin__order_min_amount: - literal: '1' - pin__outstanding_limit: - literal: '1' - pin__parent: - literal: '1' - pin__price_level: - literal: '1' - pin__remise_client: - literal: '1' - pin__remise_supplier: - literal: '1' - socialnetworks: - literal: 'null' - pin__status: - literal: '1' - pin__statut: - literal: '1' - pin__supplier_order_min_amount: - literal: '1' - pin__supplier_rate: - literal: '1' - pin__transport_mode: - literal: '1' - pin__transport_mode_supplier: - literal: '1' - pin__tva_assuj: - literal: '1' - pin__vat_reverse_charge: - literal: '1' - expected_count: 1 -frame: - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_societe - views: {} - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml deleted file mode 100644 index 1a4033f636..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml +++ /dev/null @@ -1,460 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, create, fields customer, invoice_date, town, linesx2. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` - AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, - CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` - AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, - CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` - AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, - CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` - AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) - AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` - AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) - AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS - `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, - CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, - CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, - CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, - CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, - CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, - CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` - AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` - AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, - CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS - `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS - `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) - AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` - AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` - AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, - CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS - `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, - CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, - CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, - CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) - AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, - CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CASE WHEN (SELECT - COUNT(*) FROM `llx_societe` u1 WHERE CAST(CAST(u1.`nom` AS CHAR) AS BINARY) = CAST(CAST(a1.`nom` - AS CHAR) AS BINARY) AND CAST(CAST(u1.`town` AS CHAR) AS BINARY) = CAST(CAST(a1.`town` AS CHAR) - AS BINARY)) = 1 THEN CAST(a1.`nom` AS CHAR) END FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) - AS `p__customer`, CASE WHEN t.`datef` = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, - (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` u1 WHERE CAST(CAST(u1.`nom` AS CHAR) AS - BINARY) = CAST(CAST(a1.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u1.`town` AS CHAR) AS BINARY) = - CAST(CAST(a1.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`town` AS CHAR) END FROM `llx_societe` - a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town`, CASE WHEN t.`close_missing_amount` IS NULL THEN - '1' ELSE '0' END AS `pin__close_missing_amount`, CASE WHEN t.`date_closing` IS NULL THEN '1' ELSE - '0' END AS `pin__date_closing`, CASE WHEN t.`date_pointoftax` IS NULL THEN '1' ELSE '0' END AS - `pin__date_pointoftax`, CASE WHEN t.`date_valid` IS NULL THEN '1' ELSE '0' END AS `pin__date_valid`, - CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE - WHEN t.`fk_account` IS NULL THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_cond_reglement` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_cond_reglement`, CASE WHEN t.`fk_fac_rec_source` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_fac_rec_source`, CASE WHEN t.`fk_facture_source` IS - NULL THEN '1' ELSE '0' END AS `pin__fk_facture_source`, CASE WHEN t.`fk_incoterms` = (SELECT s.`fk_incoterms` - FROM `llx_societe` s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_incoterms`, - CASE WHEN t.`fk_mode_reglement` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_mode_reglement`, - CASE WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` FROM `llx_societe` s WHERE s.`rowid` - = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_projet` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_projet`, CASE WHEN t.`fk_statut` = (SELECT s.`statut` FROM `llx_societe` - s WHERE s.`rowid` = t.`fk_soc`) THEN '1' ELSE '0' END AS `pin__fk_statut`, CASE WHEN t.`fk_transport_mode` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_transport_mode`, CASE WHEN t.`fk_user_closing` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_user_closing`, CASE WHEN t.`fk_user_modif` IS NULL THEN '1' - ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_user_valid` IS NULL THEN '1' ELSE '0' END - AS `pin__fk_user_valid`, CASE WHEN t.`is_also_delivery_note` = CAST('0' AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `pin__is_also_delivery_note`, CASE WHEN t.`localtax1` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax1`, CASE WHEN t.`localtax2` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax2`, CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, CASE WHEN t.`paye` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__paye`, CASE WHEN t.`prorata_discount` IS NULL THEN '1' ELSE '0' - END AS `pin__prorata_discount`, CASE WHEN t.`remise` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE - '0' END AS `pin__remise`, CASE WHEN t.`remise_absolue` = CAST('0' AS DECIMAL(65,30)) THEN '1' - ELSE '0' END AS `pin__remise_absolue`, CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`retained_warranty` = CAST('0' AS - DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty`, CASE WHEN t.`retained_warranty_date_limit` - IS NULL THEN '1' ELSE '0' END AS `pin__retained_warranty_date_limit`, CASE WHEN t.`retained_warranty_fk_cond_reglement` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty_fk_cond_reglement`, - CASE WHEN t.`revenuestamp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__revenuestamp`, - CASE WHEN t.`situation_counter` IS NULL THEN '1' ELSE '0' END AS `pin__situation_counter`, CASE - WHEN t.`situation_cycle_ref` IS NULL THEN '1' ELSE '0' END AS `pin__situation_cycle_ref`, CASE - WHEN t.`situation_final` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_final`, - CASE WHEN t.`subtype` IS NULL THEN '1' ELSE '0' END AS `pin__subtype`, CASE WHEN t.`type` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__type` FROM `llx_facture` t - sql_query_params: - v0: &id002 - param: invoice_date - rows_llx_facturedet: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_facture` - AS CHAR) AS `fk_facture`, CAST(t.`fk_parent_line` AS CHAR) AS `fk_parent_line`, CAST(t.`fk_product` - AS CHAR) AS `fk_product`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS - `description`, CAST(t.`vat_src_code` AS CHAR) AS `vat_src_code`, CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, - CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, - CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, - CAST(t.`qty` AS CHAR) AS `qty`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise` - AS CHAR) AS `remise`, CAST(t.`fk_remise_except` AS CHAR) AS `fk_remise_except`, CAST(t.`subprice` - AS CHAR) AS `subprice`, CAST(t.`subprice_ttc` AS CHAR) AS `subprice_ttc`, CAST(t.`price` AS CHAR) - AS `price`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, - CAST(t.`total_localtax1` AS CHAR) AS `total_localtax1`, CAST(t.`total_localtax2` AS CHAR) AS `total_localtax2`, - CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, CAST(t.`product_type` AS CHAR) AS `product_type`, - CAST(t.`date_start` AS CHAR) AS `date_start`, CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`info_bits` - AS CHAR) AS `info_bits`, CAST(t.`buy_price_ht` AS CHAR) AS `buy_price_ht`, CAST(t.`fk_product_fournisseur_price` - AS CHAR) AS `fk_product_fournisseur_price`, CAST(t.`batch` AS CHAR) AS `batch`, CAST(t.`fk_warehouse` - AS CHAR) AS `fk_warehouse`, CAST(t.`special_code` AS CHAR) AS `special_code`, CAST(t.`rang` AS - CHAR) AS `rang`, CAST(t.`fk_contract_line` AS CHAR) AS `fk_contract_line`, CAST(t.`fk_unit` AS - CHAR) AS `fk_unit`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_code_ventilation` - AS CHAR) AS `fk_code_ventilation`, CAST(t.`situation_percent` AS CHAR) AS `situation_percent`, - CAST(t.`fk_prev_id` AS CHAR) AS `fk_prev_id`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, - CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, - CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_subprice` - AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, - CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` - AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` - u2 WHERE CAST(CAST(u2.`nom` AS CHAR) AS BINARY) = CAST(CAST(a2.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u2.`town` - AS CHAR) AS BINARY) = CAST(CAST(a2.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a2.`nom` AS CHAR) - END FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` - = t.`fk_facture`) AS `main__customer`, CASE WHEN (SELECT a1.`datef` FROM `llx_facture` a1 WHERE - a1.`rowid` = t.`fk_facture`) = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `main__invoice_date`, - (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` u2 WHERE CAST(CAST(u2.`nom` AS CHAR) - AS BINARY) = CAST(CAST(a2.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u2.`town` AS CHAR) AS BINARY) - = CAST(CAST(a2.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a2.`town` AS CHAR) END FROM `llx_societe` - a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS - `main__town`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_product` u1 WHERE CAST(CAST(u1.`ref` - AS CHAR) AS BINARY) = CAST(CAST(a1.`ref` AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`ref` AS CHAR) - END FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS - `p__lines_0_product_ref`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` - = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_0_qty`, CASE WHEN t.`buy_price_ht` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE WHEN t.`date_end` - IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS NULL THEN '1' ELSE - '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_multicurrency` = (SELECT s.`fk_multicurrency` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, - CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE '0' END AS `pin__fk_parent_line`, CASE WHEN - t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, CASE WHEN t.`fk_product_fournisseur_price` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, CASE WHEN t.`fk_remise_except` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE WHEN t.`fk_unit` IS NULL THEN '1' - ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` FROM - `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, - CASE WHEN t.`fk_user_modif` = CAST('2' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_user_modif`, - CASE WHEN t.`fk_warehouse` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_warehouse`, - CASE WHEN t.`info_bits` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__info_bits`, - CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_tx`, - CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_tx`, - CASE WHEN t.`multicurrency_code` = (SELECT s.`multicurrency_code` FROM `llx_facture` s WHERE s.`rowid` - = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__multicurrency_code`, CASE WHEN t.`multicurrency_subprice` - = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END - AS `pin__multicurrency_subprice`, CASE WHEN t.`multicurrency_subprice_ttc` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE WHEN t.`price` IS NULL THEN '1' - ELSE '0' END AS `pin__price`, CASE WHEN t.`product_type` = (SELECT s.`type` FROM `llx_facture` - s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__product_type`, CASE WHEN t.`rang` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN t.`remise` = (SELECT - s.`remise` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__remise`, - CASE WHEN t.`remise_percent` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_percent`, - CASE WHEN t.`situation_percent` = CAST('100' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_percent`, - CASE WHEN t.`special_code` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__special_code`, - CASE WHEN t.`subprice` = (SELECT s.`price` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) - THEN '1' ELSE '0' END AS `pin__subprice`, CASE WHEN t.`subprice_ttc` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__subprice_ttc`, CASE WHEN t.`total_localtax1` = (SELECT s.`localtax1` - FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) THEN '1' ELSE '0' END AS `pin__total_localtax1`, - CASE WHEN t.`total_localtax2` = (SELECT s.`localtax2` FROM `llx_facture` s WHERE s.`rowid` = t.`fk_facture`) - THEN '1' ELSE '0' END AS `pin__total_localtax2`, CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM - `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx`, (SELECT - CASE WHEN (SELECT COUNT(*) FROM `llx_product` u1 WHERE CAST(CAST(u1.`ref` AS CHAR) AS BINARY) - = CAST(CAST(a1.`ref` AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`ref` AS CHAR) END FROM `llx_product` - a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, - CASE WHEN CAST(%(v2)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `p__lines_1_qty` FROM `llx_facturedet` t - sql_query_params: - v0: *id002 - v1: - param: lines.0.qty - v2: - param: lines.1.qty -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__customer: - param: customer - p__invoice_date: - literal: '1' - p__town: - param: town - pin__close_missing_amount: - literal: '1' - pin__date_closing: - literal: '1' - pin__date_pointoftax: - literal: '1' - pin__date_valid: - literal: '1' - pin__entity: - literal: '1' - pin__fk_account: - literal: '1' - pin__fk_cond_reglement: - literal: '1' - pin__fk_fac_rec_source: - literal: '1' - pin__fk_facture_source: - literal: '1' - pin__fk_incoterms: - literal: '1' - pin__fk_mode_reglement: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_projet: - literal: '1' - pin__fk_statut: - literal: '1' - pin__fk_transport_mode: - literal: '1' - fk_user_author: - param: bot_user_id - pin__fk_user_closing: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_user_valid: - literal: '1' - pin__is_also_delivery_note: - literal: '1' - pin__localtax1: - literal: '1' - pin__localtax2: - literal: '1' - multicurrency_code: - literal: EUR - pin__multicurrency_tx: - literal: '1' - pin__paye: - literal: '1' - pin__prorata_discount: - literal: '1' - pin__remise: - literal: '1' - pin__remise_absolue: - literal: '1' - pin__remise_percent: - literal: '1' - pin__retained_warranty: - literal: '1' - pin__retained_warranty_date_limit: - literal: '1' - pin__retained_warranty_fk_cond_reglement: - literal: '1' - pin__revenuestamp: - literal: '1' - pin__situation_counter: - literal: '1' - pin__situation_cycle_ref: - literal: '1' - pin__situation_final: - literal: '1' - pin__subtype: - literal: '1' - pin__type: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_llx_facturedet|a row beside the main record holds lines - match: - main__customer: - param: customer - main__invoice_date: - literal: '1' - main__town: - param: town - p__lines_0_product_ref: - param: lines.0.product_ref - p__lines_0_qty: - literal: '1' - pin__buy_price_ht: - literal: '1' - pin__date_end: - literal: '1' - pin__date_start: - literal: '1' - pin__fk_code_ventilation: - literal: '1' - pin__fk_contract_line: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_parent_line: - literal: '1' - pin__fk_prev_id: - literal: '1' - pin__fk_product_fournisseur_price: - literal: '1' - pin__fk_remise_except: - literal: '1' - pin__fk_unit: - literal: '1' - pin__fk_user_author: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_warehouse: - literal: '1' - pin__info_bits: - literal: '1' - pin__localtax1_tx: - literal: '1' - localtax1_type: - literal: '0' - pin__localtax2_tx: - literal: '1' - localtax2_type: - literal: '0' - pin__multicurrency_code: - literal: '1' - pin__multicurrency_subprice: - literal: '1' - pin__multicurrency_subprice_ttc: - literal: '1' - pin__price: - literal: '1' - pin__product_type: - literal: '1' - pin__rang: - literal: '1' - pin__remise: - literal: '1' - pin__remise_percent: - literal: '1' - pin__situation_percent: - literal: '1' - pin__special_code: - literal: '1' - pin__subprice: - literal: '1' - pin__subprice_ttc: - literal: '1' - pin__total_localtax1: - literal: '1' - pin__total_localtax2: - literal: '1' - pin__tva_tx: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_llx_facturedet|a row beside the main record holds lines - match: - main__customer: - param: customer - main__invoice_date: - literal: '1' - main__town: - param: town - p__lines_1_product_ref: - param: lines.1.product_ref - p__lines_1_qty: - literal: '1' - pin__buy_price_ht: - literal: '1' - pin__date_end: - literal: '1' - pin__date_start: - literal: '1' - pin__fk_code_ventilation: - literal: '1' - pin__fk_contract_line: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_parent_line: - literal: '1' - pin__fk_prev_id: - literal: '1' - pin__fk_product_fournisseur_price: - literal: '1' - pin__fk_remise_except: - literal: '1' - pin__fk_unit: - literal: '1' - pin__fk_user_author: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_warehouse: - literal: '1' - pin__info_bits: - literal: '1' - pin__localtax1_tx: - literal: '1' - localtax1_type: - literal: '0' - pin__localtax2_tx: - literal: '1' - localtax2_type: - literal: '0' - pin__multicurrency_code: - literal: '1' - pin__multicurrency_subprice: - literal: '1' - pin__multicurrency_subprice_ttc: - literal: '1' - pin__price: - literal: '1' - pin__product_type: - literal: '1' - pin__rang: - literal: '1' - pin__remise: - literal: '1' - pin__remise_percent: - literal: '1' - pin__situation_percent: - literal: '1' - pin__special_code: - literal: '1' - pin__subprice: - literal: '1' - pin__subprice_ttc: - literal: '1' - pin__total_localtax1: - literal: '1' - pin__total_localtax2: - literal: '1' - pin__tva_tx: - literal: '1' - expected_count: 1 - count_new_only: true -frame: - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_facture - - table: llx_facturedet - views: {} - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml deleted file mode 100644 index 44dbe49ddd..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml +++ /dev/null @@ -1,389 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_draft_invoice, found, fields customer, invoice_date, town, linesx2. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` - AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, - CAST(t.`ref_client` AS CHAR) AS `ref_client`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`subtype` - AS CHAR) AS `subtype`, CAST(t.`fk_soc` AS CHAR) AS `fk_soc`, CAST(t.`datec` AS CHAR) AS `datec`, - CAST(t.`datef` AS CHAR) AS `datef`, CAST(t.`date_pointoftax` AS CHAR) AS `date_pointoftax`, CAST(t.`date_valid` - AS CHAR) AS `date_valid`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`date_closing` AS CHAR) AS `date_closing`, - CAST(t.`paye` AS CHAR) AS `paye`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise_absolue` - AS CHAR) AS `remise_absolue`, CAST(t.`remise` AS CHAR) AS `remise`, CAST(t.`close_code` AS CHAR) - AS `close_code`, CAST(t.`close_missing_amount` AS CHAR) AS `close_missing_amount`, CAST(t.`close_note` - AS CHAR) AS `close_note`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, CAST(t.`localtax1` AS CHAR) - AS `localtax1`, CAST(t.`localtax2` AS CHAR) AS `localtax2`, CAST(t.`revenuestamp` AS CHAR) AS - `revenuestamp`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, - CAST(t.`fk_statut` AS CHAR) AS `fk_statut`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, - CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_user_valid` AS CHAR) AS `fk_user_valid`, - CAST(t.`fk_user_closing` AS CHAR) AS `fk_user_closing`, CAST(t.`module_source` AS CHAR) AS `module_source`, - CAST(t.`pos_source` AS CHAR) AS `pos_source`, CAST(t.`fk_fac_rec_source` AS CHAR) AS `fk_fac_rec_source`, - CAST(t.`fk_facture_source` AS CHAR) AS `fk_facture_source`, CAST(t.`fk_projet` AS CHAR) AS `fk_projet`, - CAST(t.`increment` AS CHAR) AS `increment`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, CAST(t.`fk_currency` - AS CHAR) AS `fk_currency`, CAST(t.`fk_cond_reglement` AS CHAR) AS `fk_cond_reglement`, CAST(t.`fk_mode_reglement` - AS CHAR) AS `fk_mode_reglement`, CAST(t.`date_lim_reglement` AS CHAR) AS `date_lim_reglement`, - CAST(t.`payment_reference` AS CHAR) AS `payment_reference`, CAST(t.`note_private` AS CHAR) AS - `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`model_pdf` AS CHAR) AS - `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CAST(t.`fk_incoterms` AS CHAR) - AS `fk_incoterms`, CAST(t.`location_incoterms` AS CHAR) AS `location_incoterms`, CAST(t.`fk_transport_mode` - AS CHAR) AS `fk_transport_mode`, CAST(t.`prorata_discount` AS CHAR) AS `prorata_discount`, CAST(t.`situation_cycle_ref` - AS CHAR) AS `situation_cycle_ref`, CAST(t.`situation_counter` AS CHAR) AS `situation_counter`, - CAST(t.`situation_final` AS CHAR) AS `situation_final`, CAST(t.`retained_warranty` AS CHAR) AS - `retained_warranty`, CAST(t.`retained_warranty_date_limit` AS CHAR) AS `retained_warranty_date_limit`, - CAST(t.`retained_warranty_fk_cond_reglement` AS CHAR) AS `retained_warranty_fk_cond_reglement`, - CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` AS CHAR) AS `extraparams`, - CAST(t.`is_also_delivery_note` AS CHAR) AS `is_also_delivery_note`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_total_ht` AS CHAR) - AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` AS CHAR) AS `multicurrency_total_tva`, - CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, (SELECT CASE WHEN (SELECT - COUNT(*) FROM `llx_societe` u1 WHERE CAST(CAST(u1.`nom` AS CHAR) AS BINARY) = CAST(CAST(a1.`nom` - AS CHAR) AS BINARY) AND CAST(CAST(u1.`town` AS CHAR) AS BINARY) = CAST(CAST(a1.`town` AS CHAR) - AS BINARY)) = 1 THEN CAST(a1.`nom` AS CHAR) END FROM `llx_societe` a1 WHERE a1.`rowid` = t.`fk_soc`) - AS `p__customer`, CASE WHEN t.`datef` = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `p__invoice_date`, - (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` u1 WHERE CAST(CAST(u1.`nom` AS CHAR) AS - BINARY) = CAST(CAST(a1.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u1.`town` AS CHAR) AS BINARY) = - CAST(CAST(a1.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`town` AS CHAR) END FROM `llx_societe` - a1 WHERE a1.`rowid` = t.`fk_soc`) AS `p__town`, CASE WHEN t.`close_missing_amount` IS NULL THEN - '1' ELSE '0' END AS `pin__close_missing_amount`, CASE WHEN t.`date_closing` IS NULL THEN '1' ELSE - '0' END AS `pin__date_closing`, CASE WHEN t.`date_pointoftax` IS NULL THEN '1' ELSE '0' END AS - `pin__date_pointoftax`, CASE WHEN t.`date_valid` IS NULL THEN '1' ELSE '0' END AS `pin__date_valid`, - CASE WHEN t.`entity` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE - WHEN t.`fk_account` IS NULL THEN '1' ELSE '0' END AS `pin__fk_account`, CASE WHEN t.`fk_cond_reglement` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_cond_reglement`, CASE WHEN t.`fk_fac_rec_source` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_fac_rec_source`, CASE WHEN t.`fk_facture_source` IS - NULL THEN '1' ELSE '0' END AS `pin__fk_facture_source`, CASE WHEN t.`fk_mode_reglement` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_mode_reglement`, CASE WHEN t.`fk_projet` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_projet`, CASE WHEN t.`fk_transport_mode` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_transport_mode`, CASE WHEN t.`fk_user_closing` IS NULL THEN '1' ELSE - '0' END AS `pin__fk_user_closing`, CASE WHEN t.`fk_user_modif` IS NULL THEN '1' ELSE '0' END AS - `pin__fk_user_modif`, CASE WHEN t.`fk_user_valid` IS NULL THEN '1' ELSE '0' END AS `pin__fk_user_valid`, - CASE WHEN t.`is_also_delivery_note` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__is_also_delivery_note`, - CASE WHEN t.`localtax1` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1`, - CASE WHEN t.`localtax2` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2`, - CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, - CASE WHEN t.`paye` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__paye`, CASE WHEN - t.`prorata_discount` IS NULL THEN '1' ELSE '0' END AS `pin__prorata_discount`, CASE WHEN t.`remise` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise`, CASE WHEN t.`remise_absolue` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_absolue`, CASE WHEN t.`remise_percent` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`retained_warranty` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty`, CASE WHEN t.`retained_warranty_date_limit` - IS NULL THEN '1' ELSE '0' END AS `pin__retained_warranty_date_limit`, CASE WHEN t.`retained_warranty_fk_cond_reglement` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__retained_warranty_fk_cond_reglement`, - CASE WHEN t.`revenuestamp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__revenuestamp`, - CASE WHEN t.`situation_counter` IS NULL THEN '1' ELSE '0' END AS `pin__situation_counter`, CASE - WHEN t.`situation_cycle_ref` IS NULL THEN '1' ELSE '0' END AS `pin__situation_cycle_ref`, CASE - WHEN t.`situation_final` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_final`, - CASE WHEN t.`subtype` IS NULL THEN '1' ELSE '0' END AS `pin__subtype`, CASE WHEN t.`type` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__type` FROM `llx_facture` t - sql_query_params: - v0: &id002 - param: invoice_date - rows_llx_facturedet: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_facture` - AS CHAR) AS `fk_facture`, CAST(t.`fk_parent_line` AS CHAR) AS `fk_parent_line`, CAST(t.`fk_product` - AS CHAR) AS `fk_product`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS - `description`, CAST(t.`vat_src_code` AS CHAR) AS `vat_src_code`, CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, - CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, - CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, - CAST(t.`qty` AS CHAR) AS `qty`, CAST(t.`remise_percent` AS CHAR) AS `remise_percent`, CAST(t.`remise` - AS CHAR) AS `remise`, CAST(t.`fk_remise_except` AS CHAR) AS `fk_remise_except`, CAST(t.`subprice` - AS CHAR) AS `subprice`, CAST(t.`subprice_ttc` AS CHAR) AS `subprice_ttc`, CAST(t.`price` AS CHAR) - AS `price`, CAST(t.`total_ht` AS CHAR) AS `total_ht`, CAST(t.`total_tva` AS CHAR) AS `total_tva`, - CAST(t.`total_localtax1` AS CHAR) AS `total_localtax1`, CAST(t.`total_localtax2` AS CHAR) AS `total_localtax2`, - CAST(t.`total_ttc` AS CHAR) AS `total_ttc`, CAST(t.`product_type` AS CHAR) AS `product_type`, - CAST(t.`date_start` AS CHAR) AS `date_start`, CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`info_bits` - AS CHAR) AS `info_bits`, CAST(t.`buy_price_ht` AS CHAR) AS `buy_price_ht`, CAST(t.`fk_product_fournisseur_price` - AS CHAR) AS `fk_product_fournisseur_price`, CAST(t.`batch` AS CHAR) AS `batch`, CAST(t.`fk_warehouse` - AS CHAR) AS `fk_warehouse`, CAST(t.`special_code` AS CHAR) AS `special_code`, CAST(t.`rang` AS - CHAR) AS `rang`, CAST(t.`fk_contract_line` AS CHAR) AS `fk_contract_line`, CAST(t.`fk_unit` AS - CHAR) AS `fk_unit`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_code_ventilation` - AS CHAR) AS `fk_code_ventilation`, CAST(t.`situation_percent` AS CHAR) AS `situation_percent`, - CAST(t.`fk_prev_id` AS CHAR) AS `fk_prev_id`, CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, - CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` AS CHAR) AS `fk_multicurrency`, - CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, CAST(t.`multicurrency_subprice` - AS CHAR) AS `multicurrency_subprice`, CAST(t.`multicurrency_subprice_ttc` AS CHAR) AS `multicurrency_subprice_ttc`, - CAST(t.`multicurrency_total_ht` AS CHAR) AS `multicurrency_total_ht`, CAST(t.`multicurrency_total_tva` - AS CHAR) AS `multicurrency_total_tva`, CAST(t.`multicurrency_total_ttc` AS CHAR) AS `multicurrency_total_ttc`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` - u2 WHERE CAST(CAST(u2.`nom` AS CHAR) AS BINARY) = CAST(CAST(a2.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u2.`town` - AS CHAR) AS BINARY) = CAST(CAST(a2.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a2.`nom` AS CHAR) - END FROM `llx_societe` a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` - = t.`fk_facture`) AS `main__customer`, CASE WHEN (SELECT a1.`datef` FROM `llx_facture` a1 WHERE - a1.`rowid` = t.`fk_facture`) = CAST(%(v0)s AS DATE) THEN '1' ELSE '0' END AS `main__invoice_date`, - (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_societe` u2 WHERE CAST(CAST(u2.`nom` AS CHAR) - AS BINARY) = CAST(CAST(a2.`nom` AS CHAR) AS BINARY) AND CAST(CAST(u2.`town` AS CHAR) AS BINARY) - = CAST(CAST(a2.`town` AS CHAR) AS BINARY)) = 1 THEN CAST(a2.`town` AS CHAR) END FROM `llx_societe` - a2 WHERE a2.`rowid` = a1.`fk_soc`) FROM `llx_facture` a1 WHERE a1.`rowid` = t.`fk_facture`) AS - `main__town`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `llx_product` u1 WHERE CAST(CAST(u1.`ref` - AS CHAR) AS BINARY) = CAST(CAST(a1.`ref` AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`ref` AS CHAR) - END FROM `llx_product` a1 WHERE CAST(a1.`rowid` AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS - `p__lines_0_product_ref`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` - = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__lines_0_qty`, CASE WHEN t.`buy_price_ht` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__buy_price_ht`, CASE WHEN t.`date_end` - IS NULL THEN '1' ELSE '0' END AS `pin__date_end`, CASE WHEN t.`date_start` IS NULL THEN '1' ELSE - '0' END AS `pin__date_start`, CASE WHEN t.`fk_code_ventilation` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__fk_code_ventilation`, CASE WHEN t.`fk_contract_line` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_contract_line`, CASE WHEN t.`fk_parent_line` IS NULL THEN '1' ELSE - '0' END AS `pin__fk_parent_line`, CASE WHEN t.`fk_prev_id` IS NULL THEN '1' ELSE '0' END AS `pin__fk_prev_id`, - CASE WHEN t.`fk_product_fournisseur_price` IS NULL THEN '1' ELSE '0' END AS `pin__fk_product_fournisseur_price`, - CASE WHEN t.`fk_remise_except` IS NULL THEN '1' ELSE '0' END AS `pin__fk_remise_except`, CASE - WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, CASE WHEN t.`fk_user_modif` - = CAST('2' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_user_modif`, CASE WHEN t.`fk_warehouse` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_warehouse`, CASE WHEN t.`info_bits` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__info_bits`, CASE WHEN t.`localtax1_tx` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`multicurrency_subprice_ttc` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_subprice_ttc`, CASE - WHEN t.`price` IS NULL THEN '1' ELSE '0' END AS `pin__price`, CASE WHEN t.`rang` = CAST('0' AS - DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__rang`, CASE WHEN t.`remise_percent` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__remise_percent`, CASE WHEN t.`situation_percent` - = CAST('100' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__situation_percent`, CASE WHEN t.`special_code` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__special_code`, CASE WHEN t.`subprice_ttc` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__subprice_ttc`, (SELECT CASE WHEN - (SELECT COUNT(*) FROM `llx_product` u1 WHERE CAST(CAST(u1.`ref` AS CHAR) AS BINARY) = CAST(CAST(a1.`ref` - AS CHAR) AS BINARY)) = 1 THEN CAST(a1.`ref` AS CHAR) END FROM `llx_product` a1 WHERE CAST(a1.`rowid` - AS BINARY) = CAST(t.`fk_product` AS BINARY)) AS `p__lines_1_product_ref`, CASE WHEN CAST(%(v2)s - AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`qty` = CAST(%(v2)s AS DECIMAL(65,30)) THEN '1' - ELSE '0' END AS `p__lines_1_qty` FROM `llx_facturedet` t - sql_query_params: - v0: *id002 - v1: - param: lines.0.qty - v2: - param: lines.1.qty -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__customer: - param: customer - p__invoice_date: - literal: '1' - p__town: - param: town - pin__close_missing_amount: - literal: '1' - pin__date_closing: - literal: '1' - pin__date_pointoftax: - literal: '1' - pin__date_valid: - literal: '1' - pin__entity: - literal: '1' - pin__fk_account: - literal: '1' - pin__fk_cond_reglement: - literal: '1' - pin__fk_fac_rec_source: - literal: '1' - pin__fk_facture_source: - literal: '1' - pin__fk_mode_reglement: - literal: '1' - pin__fk_projet: - literal: '1' - pin__fk_transport_mode: - literal: '1' - pin__fk_user_closing: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_user_valid: - literal: '1' - pin__is_also_delivery_note: - literal: '1' - pin__localtax1: - literal: '1' - pin__localtax2: - literal: '1' - multicurrency_code: - literal: EUR - pin__multicurrency_tx: - literal: '1' - pin__paye: - literal: '1' - pin__prorata_discount: - literal: '1' - pin__remise: - literal: '1' - pin__remise_absolue: - literal: '1' - pin__remise_percent: - literal: '1' - pin__retained_warranty: - literal: '1' - pin__retained_warranty_date_limit: - literal: '1' - pin__retained_warranty_fk_cond_reglement: - literal: '1' - pin__revenuestamp: - literal: '1' - pin__situation_counter: - literal: '1' - pin__situation_cycle_ref: - literal: '1' - pin__situation_final: - literal: '1' - pin__subtype: - literal: '1' - pin__type: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=rows_llx_facturedet|a row beside the main record holds lines - match: - main__customer: - param: customer - main__invoice_date: - literal: '1' - main__town: - param: town - p__lines_0_product_ref: - param: lines.0.product_ref - p__lines_0_qty: - literal: '1' - pin__buy_price_ht: - literal: '1' - pin__date_end: - literal: '1' - pin__date_start: - literal: '1' - pin__fk_code_ventilation: - literal: '1' - pin__fk_contract_line: - literal: '1' - pin__fk_parent_line: - literal: '1' - pin__fk_prev_id: - literal: '1' - pin__fk_product_fournisseur_price: - literal: '1' - pin__fk_remise_except: - literal: '1' - pin__fk_unit: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_warehouse: - literal: '1' - pin__info_bits: - literal: '1' - pin__localtax1_tx: - literal: '1' - localtax1_type: - literal: '0' - pin__localtax2_tx: - literal: '1' - localtax2_type: - literal: '0' - pin__multicurrency_subprice_ttc: - literal: '1' - pin__price: - literal: '1' - pin__rang: - literal: '1' - pin__remise_percent: - literal: '1' - pin__situation_percent: - literal: '1' - pin__special_code: - literal: '1' - pin__subprice_ttc: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=rows_llx_facturedet|a row beside the main record holds lines - match: - main__customer: - param: customer - main__invoice_date: - literal: '1' - main__town: - param: town - p__lines_1_product_ref: - param: lines.1.product_ref - p__lines_1_qty: - literal: '1' - pin__buy_price_ht: - literal: '1' - pin__date_end: - literal: '1' - pin__date_start: - literal: '1' - pin__fk_code_ventilation: - literal: '1' - pin__fk_contract_line: - literal: '1' - pin__fk_parent_line: - literal: '1' - pin__fk_prev_id: - literal: '1' - pin__fk_product_fournisseur_price: - literal: '1' - pin__fk_remise_except: - literal: '1' - pin__fk_unit: - literal: '1' - pin__fk_user_modif: - literal: '1' - pin__fk_warehouse: - literal: '1' - pin__info_bits: - literal: '1' - pin__localtax1_tx: - literal: '1' - localtax1_type: - literal: '0' - pin__localtax2_tx: - literal: '1' - localtax2_type: - literal: '0' - pin__multicurrency_subprice_ttc: - literal: '1' - pin__price: - literal: '1' - pin__rang: - literal: '1' - pin__remise_percent: - literal: '1' - pin__situation_percent: - literal: '1' - pin__special_code: - literal: '1' - pin__subprice_ttc: - literal: '1' - expected_count: 1 -frame: - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_facture - - table: llx_facturedet - views: {} - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml deleted file mode 100644 index 2add8b62c7..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml +++ /dev/null @@ -1,528 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_product, create, fields label, price_excl_vat, ref, vat_rate. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` - AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, - CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_parent` AS CHAR) - AS `fk_parent`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS `description`, - CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`note` AS CHAR) AS `note`, CAST(t.`customcode` - AS CHAR) AS `customcode`, CAST(t.`fk_country` AS CHAR) AS `fk_country`, CAST(t.`fk_state` AS CHAR) - AS `fk_state`, CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, - CAST(t.`price_min` AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, - CAST(t.`price_base_type` AS CHAR) AS `price_base_type`, CAST(t.`price_label` AS CHAR) AS `price_label`, - CAST(t.`cost_price` AS CHAR) AS `cost_price`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, - CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, - CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, - CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, - CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, - CAST(t.`tosell` AS CHAR) AS `tosell`, CAST(t.`tobuy` AS CHAR) AS `tobuy`, CAST(t.`tobatch` AS - CHAR) AS `tobatch`, CAST(t.`sell_or_eat_by_mandatory` AS CHAR) AS `sell_or_eat_by_mandatory`, - CAST(t.`batch_mask` AS CHAR) AS `batch_mask`, CAST(t.`fk_product_type` AS CHAR) AS `fk_product_type`, - CAST(t.`duration` AS CHAR) AS `duration`, CAST(t.`seuil_stock_alerte` AS CHAR) AS `seuil_stock_alerte`, - CAST(t.`url` AS CHAR) AS `url`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` - AS CHAR) AS `fk_barcode_type`, CAST(t.`accountancy_code_sell` AS CHAR) AS `accountancy_code_sell`, - CAST(t.`accountancy_code_sell_intra` AS CHAR) AS `accountancy_code_sell_intra`, CAST(t.`accountancy_code_sell_export` - AS CHAR) AS `accountancy_code_sell_export`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, - CAST(t.`accountancy_code_buy_intra` AS CHAR) AS `accountancy_code_buy_intra`, CAST(t.`accountancy_code_buy_export` - AS CHAR) AS `accountancy_code_buy_export`, CAST(t.`partnumber` AS CHAR) AS `partnumber`, CAST(t.`net_measure` - AS CHAR) AS `net_measure`, CAST(t.`net_measure_units` AS CHAR) AS `net_measure_units`, CAST(t.`weight` - AS CHAR) AS `weight`, CAST(t.`weight_units` AS CHAR) AS `weight_units`, CAST(t.`length` AS CHAR) - AS `length`, CAST(t.`length_units` AS CHAR) AS `length_units`, CAST(t.`width` AS CHAR) AS `width`, - CAST(t.`width_units` AS CHAR) AS `width_units`, CAST(t.`height` AS CHAR) AS `height`, CAST(t.`height_units` - AS CHAR) AS `height_units`, CAST(t.`surface` AS CHAR) AS `surface`, CAST(t.`surface_units` AS - CHAR) AS `surface_units`, CAST(t.`volume` AS CHAR) AS `volume`, CAST(t.`volume_units` AS CHAR) - AS `volume_units`, CAST(t.`stockable_product` AS CHAR) AS `stockable_product`, CAST(t.`stock` - AS CHAR) AS `stock`, CAST(t.`pmp` AS CHAR) AS `pmp`, CAST(t.`fifo` AS CHAR) AS `fifo`, CAST(t.`lifo` - AS CHAR) AS `lifo`, CAST(t.`fk_default_warehouse` AS CHAR) AS `fk_default_warehouse`, CAST(t.`fk_default_bom` - AS CHAR) AS `fk_default_bom`, CAST(t.`fk_default_workstation` AS CHAR) AS `fk_default_workstation`, - CAST(t.`canvas` AS CHAR) AS `canvas`, CAST(t.`finished` AS CHAR) AS `finished`, CAST(t.`lifetime` - AS CHAR) AS `lifetime`, CAST(t.`qc_frequency` AS CHAR) AS `qc_frequency`, CAST(t.`hidden` AS CHAR) - AS `hidden`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, - CAST(t.`fk_price_expression` AS CHAR) AS `fk_price_expression`, CAST(t.`desiredstock` AS CHAR) - AS `desiredstock`, CAST(t.`fk_unit` AS CHAR) AS `fk_unit`, CAST(t.`price_autogen` AS CHAR) AS - `price_autogen`, CAST(t.`fk_project` AS CHAR) AS `fk_project`, CAST(t.`mandatory_period` AS CHAR) - AS `mandatory_period`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CASE WHEN CAST(%(v0)s - AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' - ELSE '0' END AS `p__price_excl_vat`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate`, CASE WHEN - t.`cost_price` IS NULL THEN '1' ELSE '0' END AS `pin__cost_price`, CASE WHEN t.`desiredstock` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__desiredstock`, CASE WHEN t.`entity` - = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fifo` IS NULL - THEN '1' ELSE '0' END AS `pin__fifo`, CASE WHEN t.`finished` IS NULL THEN '1' ELSE '0' END AS - `pin__finished`, CASE WHEN t.`fk_barcode_type` IS NULL THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, - CASE WHEN t.`fk_country` IS NULL THEN '1' ELSE '0' END AS `pin__fk_country`, CASE WHEN t.`fk_default_bom` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_default_bom`, CASE WHEN t.`fk_default_warehouse` IS - NULL THEN '1' ELSE '0' END AS `pin__fk_default_warehouse`, CASE WHEN t.`fk_default_workstation` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_default_workstation`, CASE WHEN t.`fk_parent` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_parent`, CASE WHEN t.`fk_price_expression` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_price_expression`, CASE WHEN t.`fk_product_type` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_product_type`, CASE WHEN t.`fk_project` IS - NULL THEN '1' ELSE '0' END AS `pin__fk_project`, CASE WHEN t.`fk_state` IS NULL THEN '1' ELSE - '0' END AS `pin__fk_state`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, - CASE WHEN t.`height` IS NULL THEN '1' ELSE '0' END AS `pin__height`, CASE WHEN t.`height_units` - IS NULL THEN '1' ELSE '0' END AS `pin__height_units`, CASE WHEN t.`hidden` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__hidden`, CASE WHEN t.`length` IS NULL THEN '1' ELSE '0' END AS - `pin__length`, CASE WHEN t.`length_units` IS NULL THEN '1' ELSE '0' END AS `pin__length_units`, - CASE WHEN t.`lifetime` IS NULL THEN '1' ELSE '0' END AS `pin__lifetime`, CASE WHEN t.`lifo` IS - NULL THEN '1' ELSE '0' END AS `pin__lifo`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`mandatory_period` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__mandatory_period`, CASE WHEN t.`net_measure` IS NULL THEN '1' ELSE - '0' END AS `pin__net_measure`, CASE WHEN t.`net_measure_units` IS NULL THEN '1' ELSE '0' END AS - `pin__net_measure_units`, CASE WHEN t.`pmp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__pmp`, CASE WHEN t.`price_autogen` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__price_autogen`, CASE WHEN t.`price_min` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' - END AS `pin__price_min`, CASE WHEN t.`price_min_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE - '0' END AS `pin__price_min_ttc`, CASE WHEN t.`qc_frequency` IS NULL THEN '1' ELSE '0' END AS `pin__qc_frequency`, - CASE WHEN t.`recuperableonly` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__recuperableonly`, - CASE WHEN t.`sell_or_eat_by_mandatory` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `pin__sell_or_eat_by_mandatory`, CASE WHEN t.`seuil_stock_alerte` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__seuil_stock_alerte`, CASE WHEN t.`stock` IS NULL THEN '1' ELSE - '0' END AS `pin__stock`, CASE WHEN t.`stockable_product` = CAST('1' AS DECIMAL(65,30)) THEN '1' - ELSE '0' END AS `pin__stockable_product`, CASE WHEN t.`surface` IS NULL THEN '1' ELSE '0' END - AS `pin__surface`, CASE WHEN t.`surface_units` IS NULL THEN '1' ELSE '0' END AS `pin__surface_units`, - CASE WHEN t.`tobatch` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tobatch`, CASE - WHEN t.`tobuy` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tobuy`, CASE WHEN - t.`tosell` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tosell`, CASE WHEN t.`volume` - IS NULL THEN '1' ELSE '0' END AS `pin__volume`, CASE WHEN t.`volume_units` IS NULL THEN '1' ELSE - '0' END AS `pin__volume_units`, CASE WHEN t.`weight` IS NULL THEN '1' ELSE '0' END AS `pin__weight`, - CASE WHEN t.`weight_units` IS NULL THEN '1' ELSE '0' END AS `pin__weight_units`, CASE WHEN t.`width` - IS NULL THEN '1' ELSE '0' END AS `pin__width`, CASE WHEN t.`width_units` IS NULL THEN '1' ELSE - '0' END AS `pin__width_units` FROM `llx_product` t - sql_query_params: - v0: &id002 - param: price_excl_vat - v1: &id003 - param: vat_rate - rows_llx_product_price: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`entity` - AS CHAR) AS `entity`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_product` AS CHAR) AS `fk_product`, - CAST(t.`date_price` AS CHAR) AS `date_price`, CAST(t.`price_level` AS CHAR) AS `price_level`, - CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, CAST(t.`price_min` - AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, CAST(t.`price_base_type` - AS CHAR) AS `price_base_type`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, CAST(t.`tva_tx` - AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, CAST(t.`localtax1_tx` - AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, CAST(t.`localtax2_tx` - AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, CAST(t.`fk_user_author` - AS CHAR) AS `fk_user_author`, CAST(t.`price_label` AS CHAR) AS `price_label`, CAST(t.`tosell` - AS CHAR) AS `tosell`, CAST(t.`price_by_qty` AS CHAR) AS `price_by_qty`, CAST(t.`fk_price_expression` - AS CHAR) AS `fk_price_expression`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_price` AS CHAR) - AS `multicurrency_price`, CAST(t.`multicurrency_price_ttc` AS CHAR) AS `multicurrency_price_ttc`, - (SELECT CAST(a1.`label` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) AS `main__label`, - CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT a1.`price` FROM `llx_product` - a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `main__price_excl_vat`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` - = t.`fk_product`) AS `main__ref`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND (SELECT a1.`tva_tx` FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v1)s - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__vat_rate`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP - '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `p__price_excl_vat`, CASE WHEN t.`entity` = (SELECT s.`entity` FROM `llx_product` s WHERE s.`rowid` - = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fk_multicurrency` IS NULL - THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, CASE WHEN t.`fk_price_expression` IS NULL THEN - '1' ELSE '0' END AS `pin__fk_price_expression`, CASE WHEN t.`fk_user_author` = (SELECT s.`fk_user_author` - FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__fk_user_author`, - CASE WHEN t.`localtax1_tx` = (SELECT s.`localtax1_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) - THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax1_type` = (SELECT s.`localtax1_type` - FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__localtax1_type`, - CASE WHEN t.`localtax2_tx` = (SELECT s.`localtax2_tx` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) - THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`localtax2_type` = (SELECT s.`localtax2_type` - FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__localtax2_type`, - CASE WHEN t.`multicurrency_price` IS NULL THEN '1' ELSE '0' END AS `pin__multicurrency_price`, - CASE WHEN t.`multicurrency_price_ttc` IS NULL THEN '1' ELSE '0' END AS `pin__multicurrency_price_ttc`, - CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, - CASE WHEN t.`price_base_type` = (SELECT s.`price_base_type` FROM `llx_product` s WHERE s.`rowid` - = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__price_base_type`, CASE WHEN t.`price_by_qty` - = (SELECT s.`price_by_qty` FROM `llx_product_price` s WHERE s.`rowid` = t.`fk_user_author`) THEN - '1' ELSE '0' END AS `pin__price_by_qty`, CASE WHEN t.`price_level` = (SELECT s.`price_level` FROM - `llx_product_price` s WHERE s.`rowid` = t.`fk_user_author`) THEN '1' ELSE '0' END AS `pin__price_level`, - CASE WHEN t.`price_min` = (SELECT s.`price_min` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) - THEN '1' ELSE '0' END AS `pin__price_min`, CASE WHEN t.`price_min_ttc` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__price_min_ttc`, CASE WHEN t.`recuperableonly` = (SELECT s.`recuperableonly` - FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__recuperableonly`, - CASE WHEN t.`tosell` = (SELECT s.`tosell` FROM `llx_product` s WHERE s.`rowid` = t.`fk_product`) - THEN '1' ELSE '0' END AS `pin__tosell`, CASE WHEN t.`tva_tx` = (SELECT s.`tva_tx` FROM `llx_product` - s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__tva_tx`, CASE WHEN CAST(%(v1)s - AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN - '1' ELSE '0' END AS `p__vat_rate`, CASE WHEN t.`price` = (SELECT s.`price` FROM `llx_product` - s WHERE s.`rowid` = t.`fk_product`) THEN '1' ELSE '0' END AS `pin__price` FROM `llx_product_price` - t - sql_query_params: - v0: *id002 - v1: *id003 - raw_llx_actioncomm: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` - AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, - CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` - AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` - AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` - AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS - `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, - CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, - CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` - AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) - AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, - CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` - AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` - AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` - AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS - CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) - AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, - CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` - AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS - `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` - AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` - AS CHAR) AS `extraparams` FROM `llx_actioncomm` t - raw_llx_actioncomm_resources: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` - AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` - AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` - AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` - t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - label: - param: label - p__price_excl_vat: - literal: '1' - ref: - param: ref - p__vat_rate: - literal: '1' - pin__cost_price: - literal: '1' - pin__desiredstock: - literal: '1' - pin__entity: - literal: '1' - pin__fifo: - literal: '1' - pin__finished: - literal: '1' - pin__fk_barcode_type: - literal: '1' - pin__fk_country: - literal: '1' - pin__fk_default_bom: - literal: '1' - pin__fk_default_warehouse: - literal: '1' - pin__fk_default_workstation: - literal: '1' - pin__fk_parent: - literal: '1' - pin__fk_price_expression: - literal: '1' - pin__fk_product_type: - literal: '1' - pin__fk_project: - literal: '1' - pin__fk_state: - literal: '1' - pin__fk_unit: - literal: '1' - fk_user_author: - param: bot_user_id - fk_user_modif: - param: bot_user_id - pin__height: - literal: '1' - pin__height_units: - literal: '1' - pin__hidden: - literal: '1' - pin__length: - literal: '1' - pin__length_units: - literal: '1' - pin__lifetime: - literal: '1' - pin__lifo: - literal: '1' - pin__localtax1_tx: - literal: '1' - localtax1_type: - literal: '0' - pin__localtax2_tx: - literal: '1' - localtax2_type: - literal: '0' - pin__mandatory_period: - literal: '1' - pin__net_measure: - literal: '1' - pin__net_measure_units: - literal: '1' - pin__pmp: - literal: '1' - pin__price_autogen: - literal: '1' - price_base_type: - literal: HT - pin__price_min: - literal: '1' - pin__price_min_ttc: - literal: '1' - pin__qc_frequency: - literal: '1' - pin__recuperableonly: - literal: '1' - pin__sell_or_eat_by_mandatory: - literal: '1' - pin__seuil_stock_alerte: - literal: '1' - pin__stock: - literal: '1' - pin__stockable_product: - literal: '1' - pin__surface: - literal: '1' - pin__surface_units: - literal: '1' - pin__tobatch: - literal: '1' - pin__tobuy: - literal: '1' - pin__tosell: - literal: '1' - pin__volume: - literal: '1' - pin__volume_units: - literal: '1' - pin__weight: - literal: '1' - pin__weight_units: - literal: '1' - pin__width: - literal: '1' - pin__width_units: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_llx_product_price|a row beside the main record holds price_excl_vat - match: - main__label: - param: label - main__price_excl_vat: - literal: '1' - main__ref: - param: ref - main__vat_rate: - literal: '1' - p__price_excl_vat: - literal: '1' - pin__entity: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_price_expression: - literal: '1' - pin__fk_user_author: - literal: '1' - pin__localtax1_tx: - literal: '1' - pin__localtax1_type: - literal: '1' - pin__localtax2_tx: - literal: '1' - pin__localtax2_type: - literal: '1' - pin__multicurrency_price: - literal: '1' - pin__multicurrency_price_ttc: - literal: '1' - pin__multicurrency_tx: - literal: '1' - pin__price_base_type: - literal: '1' - pin__price_by_qty: - literal: '1' - pin__price_level: - literal: '1' - pin__price_min: - literal: '1' - pin__price_min_ttc: - literal: '1' - pin__recuperableonly: - literal: '1' - pin__tosell: - literal: '1' - pin__tva_tx: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_llx_product_price|a row beside the main record holds vat_rate - match: - main__label: - param: label - main__price_excl_vat: - literal: '1' - main__ref: - param: ref - main__vat_rate: - literal: '1' - p__vat_rate: - literal: '1' - pin__entity: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_price_expression: - literal: '1' - pin__fk_user_author: - literal: '1' - pin__localtax1_tx: - literal: '1' - pin__localtax1_type: - literal: '1' - pin__localtax2_tx: - literal: '1' - pin__localtax2_type: - literal: '1' - pin__multicurrency_price: - literal: '1' - pin__multicurrency_price_ttc: - literal: '1' - pin__multicurrency_tx: - literal: '1' - pin__price: - literal: '1' - pin__price_base_type: - literal: '1' - pin__price_by_qty: - literal: '1' - pin__price_level: - literal: '1' - pin__price_min: - literal: '1' - pin__price_min_ttc: - literal: '1' - pin__recuperableonly: - literal: '1' - pin__tosell: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm - match: - entity: - literal: '1' - event_paid: - literal: '0' - fk_action: - literal: '40' - fk_parent: - literal: '0' - fk_user_action: - param: bot_user_id - fk_user_author: - param: bot_user_id - fulldayevent: - literal: '0' - percent: - literal: '-1' - priority: - literal: '0' - status: - literal: '0' - transparency: - literal: '0' - visibility: - literal: default - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources - match: - element_type: - literal: user - fk_element: - param: bot_user_id - mandatory: - literal: '0' - expected_count: 1 - count_new_only: true -frame: - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_product - - table: llx_product_price - views: - llx_actioncomm: raw_llx_actioncomm - llx_actioncomm_resources: raw_llx_actioncomm_resources - settle_s: 1 - settle_attempts: 5 - new_row_fields: - llx_actioncomm: - - code - - datec - - datep - - datep2 - - durationp - - elementtype - - email_from - - email_msgid - - email_sender - - email_subject - - email_to - - email_tobcc - - email_tocc - - errors_to - - extraparams - - fk_element - - id - - import_key - - ip - - label - - location - - note - - recurid - - recurrule - - ref - - ref_ext - - reply_to - - tms - llx_actioncomm_resources: - - answer_status - - fk_actioncomm - - rowid - - transparency - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml deleted file mode 100644 index 79d8c6f07e..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml +++ /dev/null @@ -1,333 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_product, found, fields label, price_excl_vat, ref, vat_rate. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`ref` - AS CHAR) AS `ref`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, - CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_parent` AS CHAR) - AS `fk_parent`, CAST(t.`label` AS CHAR) AS `label`, CAST(t.`description` AS CHAR) AS `description`, - CAST(t.`note_public` AS CHAR) AS `note_public`, CAST(t.`note` AS CHAR) AS `note`, CAST(t.`customcode` - AS CHAR) AS `customcode`, CAST(t.`fk_country` AS CHAR) AS `fk_country`, CAST(t.`fk_state` AS CHAR) - AS `fk_state`, CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, - CAST(t.`price_min` AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, - CAST(t.`price_base_type` AS CHAR) AS `price_base_type`, CAST(t.`price_label` AS CHAR) AS `price_label`, - CAST(t.`cost_price` AS CHAR) AS `cost_price`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, - CAST(t.`tva_tx` AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, - CAST(t.`localtax1_tx` AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, - CAST(t.`localtax2_tx` AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, - CAST(t.`fk_user_author` AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, - CAST(t.`tosell` AS CHAR) AS `tosell`, CAST(t.`tobuy` AS CHAR) AS `tobuy`, CAST(t.`tobatch` AS - CHAR) AS `tobatch`, CAST(t.`sell_or_eat_by_mandatory` AS CHAR) AS `sell_or_eat_by_mandatory`, - CAST(t.`batch_mask` AS CHAR) AS `batch_mask`, CAST(t.`fk_product_type` AS CHAR) AS `fk_product_type`, - CAST(t.`duration` AS CHAR) AS `duration`, CAST(t.`seuil_stock_alerte` AS CHAR) AS `seuil_stock_alerte`, - CAST(t.`url` AS CHAR) AS `url`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` - AS CHAR) AS `fk_barcode_type`, CAST(t.`accountancy_code_sell` AS CHAR) AS `accountancy_code_sell`, - CAST(t.`accountancy_code_sell_intra` AS CHAR) AS `accountancy_code_sell_intra`, CAST(t.`accountancy_code_sell_export` - AS CHAR) AS `accountancy_code_sell_export`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, - CAST(t.`accountancy_code_buy_intra` AS CHAR) AS `accountancy_code_buy_intra`, CAST(t.`accountancy_code_buy_export` - AS CHAR) AS `accountancy_code_buy_export`, CAST(t.`partnumber` AS CHAR) AS `partnumber`, CAST(t.`net_measure` - AS CHAR) AS `net_measure`, CAST(t.`net_measure_units` AS CHAR) AS `net_measure_units`, CAST(t.`weight` - AS CHAR) AS `weight`, CAST(t.`weight_units` AS CHAR) AS `weight_units`, CAST(t.`length` AS CHAR) - AS `length`, CAST(t.`length_units` AS CHAR) AS `length_units`, CAST(t.`width` AS CHAR) AS `width`, - CAST(t.`width_units` AS CHAR) AS `width_units`, CAST(t.`height` AS CHAR) AS `height`, CAST(t.`height_units` - AS CHAR) AS `height_units`, CAST(t.`surface` AS CHAR) AS `surface`, CAST(t.`surface_units` AS - CHAR) AS `surface_units`, CAST(t.`volume` AS CHAR) AS `volume`, CAST(t.`volume_units` AS CHAR) - AS `volume_units`, CAST(t.`stockable_product` AS CHAR) AS `stockable_product`, CAST(t.`stock` - AS CHAR) AS `stock`, CAST(t.`pmp` AS CHAR) AS `pmp`, CAST(t.`fifo` AS CHAR) AS `fifo`, CAST(t.`lifo` - AS CHAR) AS `lifo`, CAST(t.`fk_default_warehouse` AS CHAR) AS `fk_default_warehouse`, CAST(t.`fk_default_bom` - AS CHAR) AS `fk_default_bom`, CAST(t.`fk_default_workstation` AS CHAR) AS `fk_default_workstation`, - CAST(t.`canvas` AS CHAR) AS `canvas`, CAST(t.`finished` AS CHAR) AS `finished`, CAST(t.`lifetime` - AS CHAR) AS `lifetime`, CAST(t.`qc_frequency` AS CHAR) AS `qc_frequency`, CAST(t.`hidden` AS CHAR) - AS `hidden`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, - CAST(t.`fk_price_expression` AS CHAR) AS `fk_price_expression`, CAST(t.`desiredstock` AS CHAR) - AS `desiredstock`, CAST(t.`fk_unit` AS CHAR) AS `fk_unit`, CAST(t.`price_autogen` AS CHAR) AS - `price_autogen`, CAST(t.`fk_project` AS CHAR) AS `fk_project`, CAST(t.`mandatory_period` AS CHAR) - AS `mandatory_period`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, CASE WHEN CAST(%(v0)s - AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' - ELSE '0' END AS `p__price_excl_vat`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND t.`tva_tx` = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate`, CASE WHEN - t.`cost_price` IS NULL THEN '1' ELSE '0' END AS `pin__cost_price`, CASE WHEN t.`desiredstock` - = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__desiredstock`, CASE WHEN t.`entity` - = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__entity`, CASE WHEN t.`fifo` IS NULL - THEN '1' ELSE '0' END AS `pin__fifo`, CASE WHEN t.`finished` IS NULL THEN '1' ELSE '0' END AS - `pin__finished`, CASE WHEN t.`fk_barcode_type` IS NULL THEN '1' ELSE '0' END AS `pin__fk_barcode_type`, - CASE WHEN t.`fk_country` IS NULL THEN '1' ELSE '0' END AS `pin__fk_country`, CASE WHEN t.`fk_default_bom` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_default_bom`, CASE WHEN t.`fk_default_warehouse` IS - NULL THEN '1' ELSE '0' END AS `pin__fk_default_warehouse`, CASE WHEN t.`fk_default_workstation` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_default_workstation`, CASE WHEN t.`fk_parent` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_parent`, CASE WHEN t.`fk_price_expression` - IS NULL THEN '1' ELSE '0' END AS `pin__fk_price_expression`, CASE WHEN t.`fk_product_type` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__fk_product_type`, CASE WHEN t.`fk_project` IS - NULL THEN '1' ELSE '0' END AS `pin__fk_project`, CASE WHEN t.`fk_state` IS NULL THEN '1' ELSE - '0' END AS `pin__fk_state`, CASE WHEN t.`fk_unit` IS NULL THEN '1' ELSE '0' END AS `pin__fk_unit`, - CASE WHEN t.`height` IS NULL THEN '1' ELSE '0' END AS `pin__height`, CASE WHEN t.`height_units` - IS NULL THEN '1' ELSE '0' END AS `pin__height_units`, CASE WHEN t.`hidden` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__hidden`, CASE WHEN t.`length` IS NULL THEN '1' ELSE '0' END AS - `pin__length`, CASE WHEN t.`length_units` IS NULL THEN '1' ELSE '0' END AS `pin__length_units`, - CASE WHEN t.`lifetime` IS NULL THEN '1' ELSE '0' END AS `pin__lifetime`, CASE WHEN t.`lifo` IS - NULL THEN '1' ELSE '0' END AS `pin__lifo`, CASE WHEN t.`localtax1_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax1_tx`, CASE WHEN t.`localtax2_tx` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__localtax2_tx`, CASE WHEN t.`mandatory_period` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__mandatory_period`, CASE WHEN t.`net_measure` IS NULL THEN '1' ELSE - '0' END AS `pin__net_measure`, CASE WHEN t.`net_measure_units` IS NULL THEN '1' ELSE '0' END AS - `pin__net_measure_units`, CASE WHEN t.`pmp` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__pmp`, CASE WHEN t.`price_autogen` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__price_autogen`, CASE WHEN t.`price_min` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' - END AS `pin__price_min`, CASE WHEN t.`price_min_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE - '0' END AS `pin__price_min_ttc`, CASE WHEN t.`qc_frequency` IS NULL THEN '1' ELSE '0' END AS `pin__qc_frequency`, - CASE WHEN t.`recuperableonly` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__recuperableonly`, - CASE WHEN t.`sell_or_eat_by_mandatory` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `pin__sell_or_eat_by_mandatory`, CASE WHEN t.`seuil_stock_alerte` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__seuil_stock_alerte`, CASE WHEN t.`stock` IS NULL THEN '1' ELSE - '0' END AS `pin__stock`, CASE WHEN t.`stockable_product` = CAST('1' AS DECIMAL(65,30)) THEN '1' - ELSE '0' END AS `pin__stockable_product`, CASE WHEN t.`surface` IS NULL THEN '1' ELSE '0' END - AS `pin__surface`, CASE WHEN t.`surface_units` IS NULL THEN '1' ELSE '0' END AS `pin__surface_units`, - CASE WHEN t.`tobatch` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tobatch`, CASE - WHEN t.`tobuy` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tobuy`, CASE WHEN - t.`tosell` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__tosell`, CASE WHEN t.`volume` - IS NULL THEN '1' ELSE '0' END AS `pin__volume`, CASE WHEN t.`volume_units` IS NULL THEN '1' ELSE - '0' END AS `pin__volume_units`, CASE WHEN t.`weight` IS NULL THEN '1' ELSE '0' END AS `pin__weight`, - CASE WHEN t.`weight_units` IS NULL THEN '1' ELSE '0' END AS `pin__weight_units`, CASE WHEN t.`width` - IS NULL THEN '1' ELSE '0' END AS `pin__width`, CASE WHEN t.`width_units` IS NULL THEN '1' ELSE - '0' END AS `pin__width_units` FROM `llx_product` t - sql_query_params: - v0: &id002 - param: price_excl_vat - v1: &id003 - param: vat_rate - rows_llx_product_price: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`entity` - AS CHAR) AS `entity`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_product` AS CHAR) AS `fk_product`, - CAST(t.`date_price` AS CHAR) AS `date_price`, CAST(t.`price_level` AS CHAR) AS `price_level`, - CAST(t.`price` AS CHAR) AS `price`, CAST(t.`price_ttc` AS CHAR) AS `price_ttc`, CAST(t.`price_min` - AS CHAR) AS `price_min`, CAST(t.`price_min_ttc` AS CHAR) AS `price_min_ttc`, CAST(t.`price_base_type` - AS CHAR) AS `price_base_type`, CAST(t.`default_vat_code` AS CHAR) AS `default_vat_code`, CAST(t.`tva_tx` - AS CHAR) AS `tva_tx`, CAST(t.`recuperableonly` AS CHAR) AS `recuperableonly`, CAST(t.`localtax1_tx` - AS CHAR) AS `localtax1_tx`, CAST(t.`localtax1_type` AS CHAR) AS `localtax1_type`, CAST(t.`localtax2_tx` - AS CHAR) AS `localtax2_tx`, CAST(t.`localtax2_type` AS CHAR) AS `localtax2_type`, CAST(t.`fk_user_author` - AS CHAR) AS `fk_user_author`, CAST(t.`price_label` AS CHAR) AS `price_label`, CAST(t.`tosell` - AS CHAR) AS `tosell`, CAST(t.`price_by_qty` AS CHAR) AS `price_by_qty`, CAST(t.`fk_price_expression` - AS CHAR) AS `fk_price_expression`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`multicurrency_tx` AS CHAR) AS `multicurrency_tx`, CAST(t.`multicurrency_price` AS CHAR) - AS `multicurrency_price`, CAST(t.`multicurrency_price_ttc` AS CHAR) AS `multicurrency_price_ttc`, - (SELECT CAST(a1.`label` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) AS `main__label`, - CASE WHEN CAST(%(v0)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT a1.`price` FROM `llx_product` - a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `main__price_excl_vat`, (SELECT CAST(a1.`ref` AS CHAR) FROM `llx_product` a1 WHERE a1.`rowid` - = t.`fk_product`) AS `main__ref`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND (SELECT a1.`tva_tx` FROM `llx_product` a1 WHERE a1.`rowid` = t.`fk_product`) = CAST(%(v1)s - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__vat_rate`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP - '^-?[0-9]+([.][0-9]+)?$' AND t.`price` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `p__price_excl_vat`, CASE WHEN t.`fk_multicurrency` IS NULL THEN '1' ELSE '0' END AS `pin__fk_multicurrency`, - CASE WHEN t.`fk_price_expression` IS NULL THEN '1' ELSE '0' END AS `pin__fk_price_expression`, - CASE WHEN t.`multicurrency_price` IS NULL THEN '1' ELSE '0' END AS `pin__multicurrency_price`, - CASE WHEN t.`multicurrency_price_ttc` IS NULL THEN '1' ELSE '0' END AS `pin__multicurrency_price_ttc`, - CASE WHEN t.`multicurrency_tx` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__multicurrency_tx`, - CASE WHEN t.`price_min_ttc` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__price_min_ttc`, - CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND t.`tva_tx` = CAST(%(v1)s AS - DECIMAL(65,30)) THEN '1' ELSE '0' END AS `p__vat_rate` FROM `llx_product_price` t - sql_query_params: - v0: *id002 - v1: *id003 -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - label: - param: label - p__price_excl_vat: - literal: '1' - ref: - param: ref - p__vat_rate: - literal: '1' - pin__cost_price: - literal: '1' - pin__desiredstock: - literal: '1' - pin__entity: - literal: '1' - pin__fifo: - literal: '1' - pin__finished: - literal: '1' - pin__fk_barcode_type: - literal: '1' - pin__fk_country: - literal: '1' - pin__fk_default_bom: - literal: '1' - pin__fk_default_warehouse: - literal: '1' - pin__fk_default_workstation: - literal: '1' - pin__fk_parent: - literal: '1' - pin__fk_price_expression: - literal: '1' - pin__fk_product_type: - literal: '1' - pin__fk_project: - literal: '1' - pin__fk_state: - literal: '1' - pin__fk_unit: - literal: '1' - pin__height: - literal: '1' - pin__height_units: - literal: '1' - pin__hidden: - literal: '1' - pin__length: - literal: '1' - pin__length_units: - literal: '1' - pin__lifetime: - literal: '1' - pin__lifo: - literal: '1' - pin__localtax1_tx: - literal: '1' - localtax1_type: - literal: '0' - pin__localtax2_tx: - literal: '1' - localtax2_type: - literal: '0' - pin__mandatory_period: - literal: '1' - pin__net_measure: - literal: '1' - pin__net_measure_units: - literal: '1' - pin__pmp: - literal: '1' - pin__price_autogen: - literal: '1' - price_base_type: - literal: HT - pin__price_min: - literal: '1' - pin__price_min_ttc: - literal: '1' - pin__qc_frequency: - literal: '1' - pin__recuperableonly: - literal: '1' - pin__sell_or_eat_by_mandatory: - literal: '1' - pin__seuil_stock_alerte: - literal: '1' - pin__stock: - literal: '1' - pin__stockable_product: - literal: '1' - pin__surface: - literal: '1' - pin__surface_units: - literal: '1' - pin__tobatch: - literal: '1' - pin__tobuy: - literal: '1' - pin__tosell: - literal: '1' - pin__volume: - literal: '1' - pin__volume_units: - literal: '1' - pin__weight: - literal: '1' - pin__weight_units: - literal: '1' - pin__width: - literal: '1' - pin__width_units: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=rows_llx_product_price|a row beside the main record holds price_excl_vat - match: - main__label: - param: label - main__price_excl_vat: - literal: '1' - main__ref: - param: ref - main__vat_rate: - literal: '1' - p__price_excl_vat: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_price_expression: - literal: '1' - pin__multicurrency_price: - literal: '1' - pin__multicurrency_price_ttc: - literal: '1' - pin__multicurrency_tx: - literal: '1' - pin__price_min_ttc: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=rows_llx_product_price|a row beside the main record holds vat_rate - match: - main__label: - param: label - main__price_excl_vat: - literal: '1' - main__ref: - param: ref - main__vat_rate: - literal: '1' - p__vat_rate: - literal: '1' - pin__fk_multicurrency: - literal: '1' - pin__fk_price_expression: - literal: '1' - pin__multicurrency_price: - literal: '1' - pin__multicurrency_price_ttc: - literal: '1' - pin__multicurrency_tx: - literal: '1' - pin__price_min_ttc: - literal: '1' - expected_count: 1 -frame: - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_product - - table: llx_product_price - views: {} - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml deleted file mode 100644 index 66bc3dc143..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml +++ /dev/null @@ -1,108 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind update_customer, found, fields customer, new_email, new_phone, town. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` - AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` - AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, - CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) - AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, - CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS - CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, - CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) - AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS - `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) - AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS - `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` - AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, - CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, - CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, - CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` - AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` - AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS - `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, - CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, - CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, - CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` - AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` - AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` - AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` - AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` - AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` - AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` - AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` - AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, - CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` - AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` - AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` - AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` - AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` - AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` - AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` - AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` - AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) - AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) - AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` - AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, - CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS - CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` - t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - nom: - param: customer - email: - param: new_email - kind: email - phone: - param: new_phone - kind: phone - town: - param: town - expected_count: 1 -frame: - target_fields: [] - versions: - read: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`fk_soc` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`fk_user_author` - AS CHAR) AS `author` FROM `llx_actioncomm` t WHERE CAST(t.`code` AS CHAR) = 'AC_COMPANY_MODIFY' - record_field: id - target_field: id - version_field: ver - author_field: author - self: - - param: bot_user_id - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_societe - views: {} - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml deleted file mode 100644 index 50b08e174d..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml +++ /dev/null @@ -1,230 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind update_customer, update, fields customer, new_email, new_phone, town. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-dolidb -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - database: dolidb - host: 127.0.0.1 - port: 9616 - user: fcg_reader - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`nom` - AS CHAR) AS `nom`, CAST(t.`name_alias` AS CHAR) AS `name_alias`, CAST(t.`entity` AS CHAR) AS `entity`, - CAST(t.`ref_ext` AS CHAR) AS `ref_ext`, CAST(t.`statut` AS CHAR) AS `statut`, CAST(t.`parent` - AS CHAR) AS `parent`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`code_client` AS CHAR) AS `code_client`, - CAST(t.`code_fournisseur` AS CHAR) AS `code_fournisseur`, CAST(t.`tp_payment_reference` AS CHAR) - AS `tp_payment_reference`, CAST(t.`accountancy_code_customer_general` AS CHAR) AS `accountancy_code_customer_general`, - CAST(t.`code_compta` AS CHAR) AS `code_compta`, CAST(t.`accountancy_code_supplier_general` AS - CHAR) AS `accountancy_code_supplier_general`, CAST(t.`code_compta_fournisseur` AS CHAR) AS `code_compta_fournisseur`, - CAST(t.`address` AS CHAR) AS `address`, CAST(t.`zip` AS CHAR) AS `zip`, CAST(t.`town` AS CHAR) - AS `town`, CAST(t.`fk_departement` AS CHAR) AS `fk_departement`, CAST(t.`fk_pays` AS CHAR) AS - `fk_pays`, CAST(t.`geolat` AS CHAR) AS `geolat`, CAST(t.`geolong` AS CHAR) AS `geolong`, HEX(t.`geopoint`) - AS `geopoint`, CAST(t.`georesultcode` AS CHAR) AS `georesultcode`, CAST(t.`phone` AS CHAR) AS - `phone`, CAST(t.`phone_mobile` AS CHAR) AS `phone_mobile`, CAST(t.`fax` AS CHAR) AS `fax`, CAST(t.`url` - AS CHAR) AS `url`, CAST(t.`email` AS CHAR) AS `email`, CAST(t.`fk_account` AS CHAR) AS `fk_account`, - CAST(t.`socialnetworks` AS CHAR) AS `socialnetworks`, CAST(t.`fk_effectif` AS CHAR) AS `fk_effectif`, - CAST(t.`fk_typent` AS CHAR) AS `fk_typent`, CAST(t.`fk_forme_juridique` AS CHAR) AS `fk_forme_juridique`, - CAST(t.`fk_currency` AS CHAR) AS `fk_currency`, CAST(t.`siren` AS CHAR) AS `siren`, CAST(t.`siret` - AS CHAR) AS `siret`, CAST(t.`ape` AS CHAR) AS `ape`, CAST(t.`idprof4` AS CHAR) AS `idprof4`, CAST(t.`idprof5` - AS CHAR) AS `idprof5`, CAST(t.`idprof6` AS CHAR) AS `idprof6`, CAST(t.`tva_intra` AS CHAR) AS - `tva_intra`, CAST(t.`capital` AS CHAR) AS `capital`, CAST(t.`fk_stcomm` AS CHAR) AS `fk_stcomm`, - CAST(t.`note_private` AS CHAR) AS `note_private`, CAST(t.`note_public` AS CHAR) AS `note_public`, - CAST(t.`model_pdf` AS CHAR) AS `model_pdf`, CAST(t.`last_main_doc` AS CHAR) AS `last_main_doc`, - CAST(t.`prefix_comm` AS CHAR) AS `prefix_comm`, CAST(t.`client` AS CHAR) AS `client`, CAST(t.`fournisseur` - AS CHAR) AS `fournisseur`, CAST(t.`supplier_account` AS CHAR) AS `supplier_account`, CAST(t.`fk_prospectlevel` - AS CHAR) AS `fk_prospectlevel`, CAST(t.`fk_incoterms` AS CHAR) AS `fk_incoterms`, CAST(t.`location_incoterms` - AS CHAR) AS `location_incoterms`, CAST(t.`customer_bad` AS CHAR) AS `customer_bad`, CAST(t.`customer_rate` - AS CHAR) AS `customer_rate`, CAST(t.`supplier_rate` AS CHAR) AS `supplier_rate`, CAST(t.`remise_client` - AS CHAR) AS `remise_client`, CAST(t.`remise_supplier` AS CHAR) AS `remise_supplier`, CAST(t.`mode_reglement` - AS CHAR) AS `mode_reglement`, CAST(t.`cond_reglement` AS CHAR) AS `cond_reglement`, CAST(t.`deposit_percent` - AS CHAR) AS `deposit_percent`, CAST(t.`transport_mode` AS CHAR) AS `transport_mode`, CAST(t.`mode_reglement_supplier` - AS CHAR) AS `mode_reglement_supplier`, CAST(t.`cond_reglement_supplier` AS CHAR) AS `cond_reglement_supplier`, - CAST(t.`transport_mode_supplier` AS CHAR) AS `transport_mode_supplier`, CAST(t.`fk_shipping_method` - AS CHAR) AS `fk_shipping_method`, CAST(t.`tva_assuj` AS CHAR) AS `tva_assuj`, CAST(t.`vat_reverse_charge` - AS CHAR) AS `vat_reverse_charge`, CAST(t.`localtax1_assuj` AS CHAR) AS `localtax1_assuj`, CAST(t.`localtax1_value` - AS CHAR) AS `localtax1_value`, CAST(t.`localtax2_assuj` AS CHAR) AS `localtax2_assuj`, CAST(t.`localtax2_value` - AS CHAR) AS `localtax2_value`, CAST(t.`barcode` AS CHAR) AS `barcode`, CAST(t.`fk_barcode_type` - AS CHAR) AS `fk_barcode_type`, CAST(t.`price_level` AS CHAR) AS `price_level`, CAST(t.`outstanding_limit` - AS CHAR) AS `outstanding_limit`, CAST(t.`order_min_amount` AS CHAR) AS `order_min_amount`, CAST(t.`supplier_order_min_amount` - AS CHAR) AS `supplier_order_min_amount`, CAST(t.`default_lang` AS CHAR) AS `default_lang`, CAST(t.`logo` - AS CHAR) AS `logo`, CAST(t.`logo_squarred` AS CHAR) AS `logo_squarred`, CAST(t.`canvas` AS CHAR) - AS `canvas`, CAST(t.`fk_warehouse` AS CHAR) AS `fk_warehouse`, CAST(t.`webservices_url` AS CHAR) - AS `webservices_url`, CAST(t.`webservices_key` AS CHAR) AS `webservices_key`, CAST(t.`accountancy_code_sell` - AS CHAR) AS `accountancy_code_sell`, CAST(t.`accountancy_code_buy` AS CHAR) AS `accountancy_code_buy`, - CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`fk_user_creat` AS - CHAR) AS `fk_user_creat`, CAST(t.`fk_user_modif` AS CHAR) AS `fk_user_modif`, CAST(t.`fk_multicurrency` - AS CHAR) AS `fk_multicurrency`, CAST(t.`multicurrency_code` AS CHAR) AS `multicurrency_code`, - CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`import_key` AS CHAR) AS `import_key` FROM `llx_societe` - t - raw_llx_actioncomm: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`ref` AS CHAR) AS `ref`, CAST(t.`ref_ext` - AS CHAR) AS `ref_ext`, CAST(t.`entity` AS CHAR) AS `entity`, CAST(t.`datep` AS CHAR) AS `datep`, - CAST(t.`datep2` AS CHAR) AS `datep2`, CAST(t.`fk_action` AS CHAR) AS `fk_action`, CAST(t.`code` - AS CHAR) AS `code`, CAST(t.`datec` AS CHAR) AS `datec`, CAST(t.`tms` AS CHAR) AS `tms`, CAST(t.`fk_user_author` - AS CHAR) AS `fk_user_author`, CAST(t.`fk_user_mod` AS CHAR) AS `fk_user_mod`, CAST(t.`fk_project` - AS CHAR) AS `fk_project`, CAST(t.`fk_task` AS CHAR) AS `fk_task`, CAST(t.`fk_soc` AS CHAR) AS - `fk_soc`, CAST(t.`fk_contact` AS CHAR) AS `fk_contact`, CAST(t.`fk_parent` AS CHAR) AS `fk_parent`, - CAST(t.`fk_user_action` AS CHAR) AS `fk_user_action`, CAST(t.`transparency` AS CHAR) AS `transparency`, - CAST(t.`priority` AS CHAR) AS `priority`, CAST(t.`visibility` AS CHAR) AS `visibility`, CAST(t.`fulldayevent` - AS CHAR) AS `fulldayevent`, CAST(t.`percent` AS CHAR) AS `percent`, CAST(t.`location` AS CHAR) - AS `location`, CAST(t.`durationp` AS CHAR) AS `durationp`, CAST(t.`label` AS CHAR) AS `label`, - CAST(t.`note` AS CHAR) AS `note`, CAST(t.`calling_duration` AS CHAR) AS `calling_duration`, CAST(t.`email_subject` - AS CHAR) AS `email_subject`, CAST(t.`email_msgid` AS CHAR) AS `email_msgid`, CAST(t.`email_from` - AS CHAR) AS `email_from`, CAST(t.`email_sender` AS CHAR) AS `email_sender`, CAST(t.`email_to` - AS CHAR) AS `email_to`, CAST(t.`email_tocc` AS CHAR) AS `email_tocc`, CAST(t.`email_tobcc` AS - CHAR) AS `email_tobcc`, CAST(t.`errors_to` AS CHAR) AS `errors_to`, CAST(t.`reply_to` AS CHAR) - AS `reply_to`, CAST(t.`recurid` AS CHAR) AS `recurid`, CAST(t.`recurrule` AS CHAR) AS `recurrule`, - CAST(t.`recurdateend` AS CHAR) AS `recurdateend`, CAST(t.`num_vote` AS CHAR) AS `num_vote`, CAST(t.`event_paid` - AS CHAR) AS `event_paid`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`fk_element` AS CHAR) AS - `fk_element`, CAST(t.`elementtype` AS CHAR) AS `elementtype`, CAST(t.`ip` AS CHAR) AS `ip`, CAST(t.`fk_bookcal_calendar` - AS CHAR) AS `fk_bookcal_calendar`, CAST(t.`import_key` AS CHAR) AS `import_key`, CAST(t.`extraparams` - AS CHAR) AS `extraparams` FROM `llx_actioncomm` t - raw_llx_actioncomm_resources: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`rowid` AS CHAR) AS `id`, CAST(t.`rowid` AS CHAR) AS `rowid`, CAST(t.`fk_actioncomm` - AS CHAR) AS `fk_actioncomm`, CAST(t.`element_type` AS CHAR) AS `element_type`, CAST(t.`fk_element` - AS CHAR) AS `fk_element`, CAST(t.`answer_status` AS CHAR) AS `answer_status`, CAST(t.`mandatory` - AS CHAR) AS `mandatory`, CAST(t.`transparency` AS CHAR) AS `transparency` FROM `llx_actioncomm_resources` - t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - nom: - param: customer - email: - param: new_email - kind: email - phone: - param: new_phone - kind: phone - town: - param: town - expected_count: 1 -- kind: record_written - probe: surface=raw_llx_actioncomm|1 row(s) the app adds to llx_actioncomm - match: - code: - literal: AC_COMPANY_MODIFY - entity: - literal: '1' - event_paid: - literal: '0' - fk_action: - literal: '40' - fk_parent: - literal: '0' - fk_user_action: - param: bot_user_id - fk_user_author: - param: bot_user_id - fulldayevent: - literal: '0' - percent: - literal: '-1' - priority: - literal: '0' - status: - literal: '0' - transparency: - literal: '0' - visibility: - literal: default - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_llx_actioncomm_resources|1 row(s) the app adds to llx_actioncomm_resources - match: - element_type: - literal: user - fk_element: - param: bot_user_id - mandatory: - literal: '0' - expected_count: 1 - count_new_only: true -frame: - target_fields: - - email - - fk_user_modif - - phone - - socialnetworks - - tms - versions: - read: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - sql_query: SELECT CAST(t.`fk_soc` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`fk_user_author` - AS CHAR) AS `author` FROM `llx_actioncomm` t WHERE CAST(t.`code` AS CHAR) = 'AC_COMPANY_MODIFY' - record_field: id - target_field: id - version_field: ver - author_field: author - self: - - param: bot_user_id - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: FCG_DOLIBARR_READER_PASSWORD - allow_tables: - - table: llx_societe - mode: skip - reason: read in full, every column, by this contract's own surface - views: - llx_actioncomm: raw_llx_actioncomm - llx_actioncomm_resources: raw_llx_actioncomm_resources - settle_s: 1 - settle_attempts: 5 - new_row_fields: - llx_actioncomm: - - datec - - datep - - datep2 - - durationp - - elementtype - - email_from - - email_msgid - - email_sender - - email_subject - - email_to - - email_tobcc - - email_tocc - - errors_to - - extraparams - - fk_soc - - id - - import_key - - ip - - label - - location - - note - - recurid - - recurrule - - ref - - ref_ext - - reply_to - - tms - llx_actioncomm_resources: - - answer_status - - fk_actioncomm - - rowid - - transparency - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml deleted file mode 100644 index 13fda668fb..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/manifest.yaml +++ /dev/null @@ -1,936 +0,0 @@ -schema_version: openadapt.contract-manifest/v1 -dialect: mysql -database: dolidb -actor_params: -- bot_user_id -questions_open: [] -kinds: - create_customer: - mode: create - main_table: llx_societe - parameters: - - name - - address - - zip - - town - - country - - phone - - email - identity: [] - lists: [] - dispatch: null - variants: - - fields: - - address - - country - - email - - name - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml - found: contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml - - fields: - - country - - email - - name - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/country+email+name+phone+town+zip.53caf579.create.yaml - found: contracts/create_customer/country+email+name+phone+town+zip.53caf579.found.yaml - - fields: - - address - - email - - name - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+email+name+phone+town+zip.e1f4b193.create.yaml - found: contracts/create_customer/address+email+name+phone+town+zip.e1f4b193.found.yaml - - fields: - - address - - country - - name - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+name+phone+town+zip.f5e33735.create.yaml - found: contracts/create_customer/address+country+name+phone+town+zip.f5e33735.found.yaml - - fields: - - address - - country - - email - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+phone+town+zip.fbf8862c.create.yaml - found: contracts/create_customer/address+country+email+phone+town+zip.fbf8862c.found.yaml - - fields: - - address - - country - - email - - name - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+name+town+zip.d1643b80.create.yaml - found: contracts/create_customer/address+country+email+name+town+zip.d1643b80.found.yaml - - fields: - - address - - country - - email - - name - - phone - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+name+phone+zip.ccf08c47.create.yaml - found: contracts/create_customer/address+country+email+name+phone+zip.ccf08c47.found.yaml - - fields: - - address - - country - - email - - name - - phone - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+name+phone+town.789a6442.create.yaml - found: contracts/create_customer/address+country+email+name+phone+town.789a6442.found.yaml - - fields: - - email - - name - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/email+name+phone+town+zip.f154bfb3.create.yaml - found: contracts/create_customer/email+name+phone+town+zip.f154bfb3.found.yaml - - fields: - - country - - name - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/country+name+phone+town+zip.0d88eb41.create.yaml - found: contracts/create_customer/country+name+phone+town+zip.0d88eb41.found.yaml - - fields: - - country - - email - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/country+email+phone+town+zip.4b6883d6.create.yaml - found: contracts/create_customer/country+email+phone+town+zip.4b6883d6.found.yaml - - fields: - - country - - email - - name - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/country+email+name+town+zip.885cb135.create.yaml - found: contracts/create_customer/country+email+name+town+zip.885cb135.found.yaml - - fields: - - country - - email - - name - - phone - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/country+email+name+phone+zip.fc167af2.create.yaml - found: contracts/create_customer/country+email+name+phone+zip.fc167af2.found.yaml - - fields: - - country - - email - - name - - phone - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/country+email+name+phone+town.44f5a950.create.yaml - found: contracts/create_customer/country+email+name+phone+town.44f5a950.found.yaml - - fields: - - address - - name - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+name+phone+town+zip.d9f5e809.create.yaml - found: contracts/create_customer/address+name+phone+town+zip.d9f5e809.found.yaml - - fields: - - address - - email - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+email+phone+town+zip.9a3bf199.create.yaml - found: contracts/create_customer/address+email+phone+town+zip.9a3bf199.found.yaml - - fields: - - address - - email - - name - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+email+name+town+zip.2c8b3188.create.yaml - found: contracts/create_customer/address+email+name+town+zip.2c8b3188.found.yaml - - fields: - - address - - email - - name - - phone - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+email+name+phone+zip.73ba341b.create.yaml - found: contracts/create_customer/address+email+name+phone+zip.73ba341b.found.yaml - - fields: - - address - - email - - name - - phone - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/address+email+name+phone+town.fb28b6ed.create.yaml - found: contracts/create_customer/address+email+name+phone+town.fb28b6ed.found.yaml - - fields: - - address - - country - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+phone+town+zip.9e804046.create.yaml - found: contracts/create_customer/address+country+phone+town+zip.9e804046.found.yaml - - fields: - - address - - country - - name - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+name+town+zip.a3c17aca.create.yaml - found: contracts/create_customer/address+country+name+town+zip.a3c17aca.found.yaml - - fields: - - address - - country - - name - - phone - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+name+phone+zip.706c04dd.create.yaml - found: contracts/create_customer/address+country+name+phone+zip.706c04dd.found.yaml - - fields: - - address - - country - - name - - phone - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+name+phone+town.5082b1a5.create.yaml - found: contracts/create_customer/address+country+name+phone+town.5082b1a5.found.yaml - - fields: - - address - - country - - email - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+town+zip.e2b6efe5.create.yaml - found: contracts/create_customer/address+country+email+town+zip.e2b6efe5.found.yaml - - fields: - - address - - country - - email - - phone - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+phone+zip.32467ede.create.yaml - found: contracts/create_customer/address+country+email+phone+zip.32467ede.found.yaml - - fields: - - address - - country - - email - - phone - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+phone+town.d258e0cb.create.yaml - found: contracts/create_customer/address+country+email+phone+town.d258e0cb.found.yaml - - fields: - - address - - country - - email - - name - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+name+zip.9c85e2ad.create.yaml - found: contracts/create_customer/address+country+email+name+zip.9c85e2ad.found.yaml - - fields: - - address - - country - - email - - name - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+name+town.7d754069.create.yaml - found: contracts/create_customer/address+country+email+name+town.7d754069.found.yaml - - fields: - - address - - country - - email - - name - - phone - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+name+phone.a2b4ef09.create.yaml - found: contracts/create_customer/address+country+email+name+phone.a2b4ef09.found.yaml - - fields: - - name - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/name+phone+town+zip.fbceaff4.create.yaml - found: contracts/create_customer/name+phone+town+zip.fbceaff4.found.yaml - - fields: - - email - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/email+phone+town+zip.3535f506.create.yaml - found: contracts/create_customer/email+phone+town+zip.3535f506.found.yaml - - fields: - - email - - name - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/email+name+town+zip.475b04b2.create.yaml - found: contracts/create_customer/email+name+town+zip.475b04b2.found.yaml - - fields: - - email - - name - - phone - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/email+name+phone+zip.d936d504.create.yaml - found: contracts/create_customer/email+name+phone+zip.d936d504.found.yaml - - fields: - - email - - name - - phone - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/email+name+phone+town.5328a595.create.yaml - found: contracts/create_customer/email+name+phone+town.5328a595.found.yaml - - fields: - - country - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/country+phone+town+zip.ac1af0a2.create.yaml - found: contracts/create_customer/country+phone+town+zip.ac1af0a2.found.yaml - - fields: - - country - - name - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/country+name+town+zip.4a8420e2.create.yaml - found: contracts/create_customer/country+name+town+zip.4a8420e2.found.yaml - - fields: - - country - - name - - phone - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/country+name+phone+zip.a7ae9bc0.create.yaml - found: contracts/create_customer/country+name+phone+zip.a7ae9bc0.found.yaml - - fields: - - country - - name - - phone - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/country+name+phone+town.9c66266e.create.yaml - found: contracts/create_customer/country+name+phone+town.9c66266e.found.yaml - - fields: - - country - - email - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/country+email+town+zip.202dbae5.create.yaml - found: contracts/create_customer/country+email+town+zip.202dbae5.found.yaml - - fields: - - country - - email - - phone - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/country+email+phone+zip.23211a11.create.yaml - found: contracts/create_customer/country+email+phone+zip.23211a11.found.yaml - - fields: - - country - - email - - phone - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/country+email+phone+town.160469ad.create.yaml - found: contracts/create_customer/country+email+phone+town.160469ad.found.yaml - - fields: - - country - - email - - name - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/country+email+name+zip.db502169.create.yaml - found: contracts/create_customer/country+email+name+zip.db502169.found.yaml - - fields: - - country - - email - - name - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/country+email+name+town.bc62857c.create.yaml - found: contracts/create_customer/country+email+name+town.bc62857c.found.yaml - - fields: - - country - - email - - name - - phone - lists: {} - dispatch_value: null - create: contracts/create_customer/country+email+name+phone.92f6dc3e.create.yaml - found: contracts/create_customer/country+email+name+phone.92f6dc3e.found.yaml - - fields: - - address - - phone - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+phone+town+zip.476dcfe8.create.yaml - found: contracts/create_customer/address+phone+town+zip.476dcfe8.found.yaml - - fields: - - address - - name - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+name+town+zip.3c79c185.create.yaml - found: contracts/create_customer/address+name+town+zip.3c79c185.found.yaml - - fields: - - address - - name - - phone - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+name+phone+zip.ee0d62a0.create.yaml - found: contracts/create_customer/address+name+phone+zip.ee0d62a0.found.yaml - - fields: - - address - - name - - phone - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/address+name+phone+town.c22c2f3c.create.yaml - found: contracts/create_customer/address+name+phone+town.c22c2f3c.found.yaml - - fields: - - address - - email - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+email+town+zip.73b7baf0.create.yaml - found: contracts/create_customer/address+email+town+zip.73b7baf0.found.yaml - - fields: - - address - - email - - phone - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+email+phone+zip.ea4966f9.create.yaml - found: contracts/create_customer/address+email+phone+zip.ea4966f9.found.yaml - - fields: - - address - - email - - phone - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/address+email+phone+town.0dfd0d0a.create.yaml - found: contracts/create_customer/address+email+phone+town.0dfd0d0a.found.yaml - - fields: - - address - - email - - name - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+email+name+zip.6b73f20c.create.yaml - found: contracts/create_customer/address+email+name+zip.6b73f20c.found.yaml - - fields: - - address - - email - - name - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/address+email+name+town.59ece868.create.yaml - found: contracts/create_customer/address+email+name+town.59ece868.found.yaml - - fields: - - address - - email - - name - - phone - lists: {} - dispatch_value: null - create: contracts/create_customer/address+email+name+phone.c37efe1f.create.yaml - found: contracts/create_customer/address+email+name+phone.c37efe1f.found.yaml - - fields: - - address - - country - - town - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+town+zip.13199686.create.yaml - found: contracts/create_customer/address+country+town+zip.13199686.found.yaml - - fields: - - address - - country - - phone - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+phone+zip.5e48d952.create.yaml - found: contracts/create_customer/address+country+phone+zip.5e48d952.found.yaml - - fields: - - address - - country - - phone - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+phone+town.5d9fa0bc.create.yaml - found: contracts/create_customer/address+country+phone+town.5d9fa0bc.found.yaml - - fields: - - address - - country - - name - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+name+zip.0c72fe0a.create.yaml - found: contracts/create_customer/address+country+name+zip.0c72fe0a.found.yaml - - fields: - - address - - country - - name - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+name+town.0d9093f0.create.yaml - found: contracts/create_customer/address+country+name+town.0d9093f0.found.yaml - - fields: - - address - - country - - name - - phone - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+name+phone.9cb52e23.create.yaml - found: contracts/create_customer/address+country+name+phone.9cb52e23.found.yaml - - fields: - - address - - country - - email - - zip - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+zip.df797a7c.create.yaml - found: contracts/create_customer/address+country+email+zip.df797a7c.found.yaml - - fields: - - address - - country - - email - - town - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+town.dd75ab46.create.yaml - found: contracts/create_customer/address+country+email+town.dd75ab46.found.yaml - - fields: - - address - - country - - email - - phone - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+phone.fa31e92e.create.yaml - found: contracts/create_customer/address+country+email+phone.fa31e92e.found.yaml - - fields: - - address - - country - - email - - name - lists: {} - dispatch_value: null - create: contracts/create_customer/address+country+email+name.736afe34.create.yaml - found: contracts/create_customer/address+country+email+name.736afe34.found.yaml - create_product: - mode: create - main_table: llx_product - parameters: - - ref - - label - - price_excl_vat - - vat_rate - identity: [] - lists: [] - dispatch: null - variants: - - fields: - - label - - price_excl_vat - - ref - - vat_rate - lists: {} - dispatch_value: null - create: contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml - found: contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml - - fields: - - label - - ref - - vat_rate - lists: {} - dispatch_value: null - create: contracts/create_product/label+ref+vat_rate.c78fc3bd.create.yaml - found: contracts/create_product/label+ref+vat_rate.c78fc3bd.found.yaml - - fields: - - label - - price_excl_vat - - ref - lists: {} - dispatch_value: null - create: contracts/create_product/label+price_excl_vat+ref.0f575d09.create.yaml - found: contracts/create_product/label+price_excl_vat+ref.0f575d09.found.yaml - - fields: - - label - - ref - lists: {} - dispatch_value: null - create: contracts/create_product/label+ref.153a9f2e.create.yaml - found: contracts/create_product/label+ref.153a9f2e.found.yaml - create_draft_invoice: - mode: create - main_table: llx_facture - parameters: - - customer - - town - - invoice_date - - lines - identity: [] - lists: - - lines - dispatch: null - variants: - - fields: - - customer - - invoice_date - - town - lists: - lines: 1 - dispatch_value: null - create: contracts/create_draft_invoice/customer+invoice_date+town+linesx1.678821c0.create.yaml - found: contracts/create_draft_invoice/customer+invoice_date+town+linesx1.678821c0.found.yaml - - fields: - - customer - - invoice_date - - town - lists: - lines: 2 - dispatch_value: null - create: contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml - found: contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml - - fields: - - customer - - invoice_date - - town - lists: - lines: 3 - dispatch_value: null - create: contracts/create_draft_invoice/customer+invoice_date+town+linesx3.2f437a2c.create.yaml - found: contracts/create_draft_invoice/customer+invoice_date+town+linesx3.2f437a2c.found.yaml - - fields: - - customer - - invoice_date - - town - lists: - lines: 4 - dispatch_value: null - create: contracts/create_draft_invoice/customer+invoice_date+town+linesx4.aea56f08.create.yaml - found: contracts/create_draft_invoice/customer+invoice_date+town+linesx4.aea56f08.found.yaml - - fields: - - customer - - invoice_date - - town - lists: - lines: 5 - dispatch_value: null - create: contracts/create_draft_invoice/customer+invoice_date+town+linesx5.58afdd66.create.yaml - found: contracts/create_draft_invoice/customer+invoice_date+town+linesx5.58afdd66.found.yaml - - fields: - - customer - - invoice_date - - town - lists: - lines: 6 - dispatch_value: null - create: contracts/create_draft_invoice/customer+invoice_date+town+linesx6.edf87893.create.yaml - found: contracts/create_draft_invoice/customer+invoice_date+town+linesx6.edf87893.found.yaml - - fields: - - customer - - invoice_date - - town - lists: - lines: 7 - dispatch_value: null - create: contracts/create_draft_invoice/customer+invoice_date+town+linesx7.086ca1eb.create.yaml - found: contracts/create_draft_invoice/customer+invoice_date+town+linesx7.086ca1eb.found.yaml - - fields: - - customer - - invoice_date - - town - lists: - lines: 8 - dispatch_value: null - create: contracts/create_draft_invoice/customer+invoice_date+town+linesx8.8d4e9b1f.create.yaml - found: contracts/create_draft_invoice/customer+invoice_date+town+linesx8.8d4e9b1f.found.yaml - - fields: - - customer - - invoice_date - - town - lists: - lines: 9 - dispatch_value: null - create: contracts/create_draft_invoice/customer+invoice_date+town+linesx9.042138e5.create.yaml - found: contracts/create_draft_invoice/customer+invoice_date+town+linesx9.042138e5.found.yaml - - fields: - - customer - - invoice_date - - town - lists: - lines: 10 - dispatch_value: null - create: contracts/create_draft_invoice/customer+invoice_date+town+linesx10.f6fa907c.create.yaml - found: contracts/create_draft_invoice/customer+invoice_date+town+linesx10.f6fa907c.found.yaml - update_customer: - mode: update - main_table: llx_societe - parameters: - - customer - - town - - new_phone - - new_email - - new_address - - new_zip - identity: - - customer - - town - lists: [] - dispatch: null - variants: - - fields: - - customer - - new_phone - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_phone+town.e481e632.update.yaml - found: contracts/update_customer/customer+new_phone+town.e481e632.found.yaml - - fields: - - customer - - new_email - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_email+town.014f1cf4.update.yaml - found: contracts/update_customer/customer+new_email+town.014f1cf4.found.yaml - - fields: - - customer - - new_address - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_address+town.7b5a4e67.update.yaml - found: contracts/update_customer/customer+new_address+town.7b5a4e67.found.yaml - - fields: - - customer - - new_zip - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_zip+town.5089de78.update.yaml - found: contracts/update_customer/customer+new_zip+town.5089de78.found.yaml - - fields: - - customer - - new_email - - new_phone - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml - found: contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml - - fields: - - customer - - new_address - - new_phone - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_address+new_phone+town.be3325cd.update.yaml - found: contracts/update_customer/customer+new_address+new_phone+town.be3325cd.found.yaml - - fields: - - customer - - new_phone - - new_zip - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_phone+new_zip+town.00a25668.update.yaml - found: contracts/update_customer/customer+new_phone+new_zip+town.00a25668.found.yaml - - fields: - - customer - - new_address - - new_email - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_address+new_email+town.304fdd4f.update.yaml - found: contracts/update_customer/customer+new_address+new_email+town.304fdd4f.found.yaml - - fields: - - customer - - new_email - - new_zip - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_email+new_zip+town.adf5982d.update.yaml - found: contracts/update_customer/customer+new_email+new_zip+town.adf5982d.found.yaml - - fields: - - customer - - new_address - - new_zip - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_address+new_zip+town.8d26c1c7.update.yaml - found: contracts/update_customer/customer+new_address+new_zip+town.8d26c1c7.found.yaml - - fields: - - customer - - new_address - - new_email - - new_phone - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.update.yaml - found: contracts/update_customer/customer+new_address+new_email+new_phone+town.be743cce.found.yaml - - fields: - - customer - - new_email - - new_phone - - new_zip - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_email+new_phone+new_zip+town.d9b5d1c2.update.yaml - found: contracts/update_customer/customer+new_email+new_phone+new_zip+town.d9b5d1c2.found.yaml - - fields: - - customer - - new_address - - new_phone - - new_zip - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_address+new_phone+new_zip+town.998004a8.update.yaml - found: contracts/update_customer/customer+new_address+new_phone+new_zip+town.998004a8.found.yaml - - fields: - - customer - - new_address - - new_email - - new_zip - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_address+new_email+new_zip+town.e4f14417.update.yaml - found: contracts/update_customer/customer+new_address+new_email+new_zip+town.e4f14417.found.yaml - - fields: - - customer - - new_address - - new_email - - new_phone - - new_zip - - town - lists: {} - dispatch_value: null - update: contracts/update_customer/customer+new_address+new_email+new_phone+new_zip+town.0d2342b6.update.yaml - found: contracts/update_customer/customer+new_address+new_email+new_phone+new_zip+town.0d2342b6.found.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json b/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json deleted file mode 100644 index ac9b1add0e..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/dolibarr/report.json +++ /dev/null @@ -1,203 +0,0 @@ -{ - "applied": [ - "GRANT TRIGGER ON `dolidb`.* TO 'fcg_reader'@'%'" - ], - "read_account": [ - "the read account holds TRIGGER (needed to see triggers; it can also create and drop triggers, so keep its password from the agent)" - ], - "dialect": "mysql", - "tables": 387, - "questions": [ - { - "number": 1, - "key": "phones", - "text": "These values were stored as other text with the same digits (a phone number written with spaces stored without): create_customer.phone, update_customer.new_phone. Compare them as phone numbers (the same digits, at least 7, and a task value that isn't shaped like a phone number is compared exactly)? Answer `phones: [, ...]` with the ones that are phone numbers; any other is compared exactly.", - "proposal": [ - "create_customer.phone", - "update_customer.new_phone" - ], - "answered": true - }, - { - "number": 2, - "key": "bookkeeping", - "text": "Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (llx_societe: the target row): llx_societe: fk_user_modif, tms. Also changed with no task value, and not excused unless you add them under columns_add (a write that changes them stops): llx_societe: socialnetworks. Rows the app adds beside each write, expected exactly that many times: llx_actioncomm (1 per write), llx_actioncomm_resources (1 per write). Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): llx_facture: close_code, close_note, date_lim_reglement, datec, extraparams, fk_currency, import_key, increment, last_main_doc, location_incoterms, model_pdf, module_source, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, note_private, note_public, payment_reference, pos_source, ref, ref_client, ref_ext, tms, total_ht, total_ttc, total_tva; llx_facturedet: batch, description, import_key, label, multicurrency_total_ht, multicurrency_total_ttc, multicurrency_total_tva, ref_ext, total_ht, total_ttc, total_tva, vat_src_code; llx_product: accountancy_code_buy, accountancy_code_buy_export, accountancy_code_buy_intra, accountancy_code_sell, accountancy_code_sell_export, accountancy_code_sell_intra, barcode, batch_mask, canvas, customcode, datec, default_vat_code, description, duration, import_key, last_main_doc, model_pdf, note, note_public, partnumber, price_label, price_ttc, ref_ext, tms, url; llx_product_price: date_price, default_vat_code, import_key, multicurrency_code, price_label, price_ttc, tms; llx_societe: accountancy_code_buy, accountancy_code_customer_general, accountancy_code_sell, accountancy_code_supplier_general, ape, barcode, canvas, code_client, code_compta, code_compta_fournisseur, code_fournisseur, datec, default_lang, deposit_percent, fax, fk_currency, fk_prospectlevel, georesultcode, idprof4, idprof5, idprof6, import_key, ip, last_main_doc, location_incoterms, logo, logo_squarred, model_pdf, multicurrency_code, name_alias, note_private, note_public, phone_mobile, prefix_comm, ref_ext, siren, siret, supplier_account, tms, tp_payment_reference, tva_intra, url, webservices_key, webservices_url. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log.", - "proposal": "accept", - "answered": true - }, - { - "number": 3, - "key": "recheck_after_s", - "text": "How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `.", - "proposal": 300, - "answered": true - }, - { - "number": 4, - "key": "locations.create_draft_invoice", - "text": "Kind `create_draft_invoice`: `invoice_date` is in several places: [0] llx_facture.datef (date); [1] llx_facture.date_lim_reglement (date) Answer `locations.create_draft_invoice: {: }` to pick one.", - "proposal": null, - "answered": true - } - ], - "kinds": { - "create_customer": { - "mode": "create", - "main_table": "llx_societe", - "locations": { - "address": "llx_societe.address (exact)", - "country": "llx_societe.fk_pays -> llx_c_country.label (exact)", - "email": "llx_societe.email (email)", - "name": "llx_societe.nom (exact)", - "phone": "llx_societe.phone (phone)", - "town": "llx_societe.town (exact)", - "zip": "llx_societe.zip (exact)" - }, - "placed_by_name": [], - "versions": null, - "placed_from_another_kind": [], - "demonstrations": 2, - "children": {}, - "dispatch_children": {}, - "lists": {}, - "side_rows": [ - { - "table": "llx_actioncomm", - "count": 1, - "unless": null - }, - { - "table": "llx_actioncomm_resources", - "count": 1, - "unless": null - } - ], - "logs": [], - "log_candidates": [], - "touched_rows": [], - "unexcused": {}, - "refused": [], - "unresolved": {}, - "emitted": 64, - "rejected": [] - }, - "create_product": { - "mode": "create", - "main_table": "llx_product", - "locations": { - "label": "llx_product.label (exact)", - "price_excl_vat": "llx_product.price (number)", - "ref": "llx_product.ref (exact)", - "vat_rate": "llx_product.tva_tx (number)" - }, - "placed_by_name": [], - "versions": null, - "placed_from_another_kind": [], - "demonstrations": 2, - "children": { - "price_excl_vat#2": "llx_product_price", - "vat_rate#2": "llx_product_price" - }, - "dispatch_children": {}, - "lists": {}, - "side_rows": [ - { - "table": "llx_actioncomm", - "count": 1, - "unless": null - }, - { - "table": "llx_actioncomm_resources", - "count": 1, - "unless": null - } - ], - "logs": [], - "log_candidates": [], - "touched_rows": [], - "unexcused": {}, - "refused": [], - "unresolved": {}, - "emitted": 4, - "rejected": [] - }, - "create_draft_invoice": { - "mode": "create", - "main_table": "llx_facture", - "locations": { - "customer": "llx_facture.fk_soc -> llx_societe.nom (exact)", - "invoice_date": "llx_facture.datef (date)", - "town": "llx_facture.fk_soc -> llx_societe.town (exact)" - }, - "placed_by_name": [], - "versions": null, - "placed_from_another_kind": [], - "demonstrations": 2, - "children": {}, - "dispatch_children": {}, - "lists": { - "lines": "llx_facturedet" - }, - "side_rows": [], - "logs": [], - "log_candidates": [], - "touched_rows": [], - "unexcused": {}, - "refused": [], - "unresolved": {}, - "emitted": 10, - "rejected": [] - }, - "update_customer": { - "mode": "update", - "main_table": "llx_societe", - "locations": { - "customer": "llx_societe.nom (exact)", - "new_address": "llx_societe.address (exact)", - "new_email": "llx_societe.email (email)", - "new_phone": "llx_societe.phone (phone)", - "new_zip": "llx_societe.zip (exact)", - "town": "llx_societe.town (exact)" - }, - "placed_by_name": [ - "new_address", - "new_zip" - ], - "versions": { - "kind": "audit", - "table": "llx_actioncomm", - "record_column": "fk_soc", - "author_column": "fk_user_author", - "actor_param": "bot_user_id", - "where": { - "code": "AC_COMPANY_MODIFY" - } - }, - "placed_from_another_kind": [], - "demonstrations": 2, - "children": {}, - "dispatch_children": {}, - "lists": {}, - "side_rows": [ - { - "table": "llx_actioncomm", - "count": 1, - "unless": null - }, - { - "table": "llx_actioncomm_resources", - "count": 1, - "unless": null - } - ], - "logs": [], - "log_candidates": [], - "touched_rows": [], - "unexcused": {}, - "refused": [], - "unresolved": {}, - "emitted": 15, - "rejected": [] - } - } -} \ No newline at end of file diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md deleted file mode 100644 index 247d519f13..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/QUESTIONS.md +++ /dev/null @@ -1,20 +0,0 @@ -# Questions for a person - -1. These values were stored as other text with the same digits (a phone number written with spaces stored without): create_contact.phone. Compare them as phone numbers (the same digits, at least 7, and a task value that isn't shaped like a phone number is compared exactly)? Answer `phones: [, ...]` with the ones that are phone numbers; any other is compared exactly. - - Answer key: `phones`. Proposed: `['create_contact.phone']`. - - Answered: `["create_contact.phone"]` - -2. Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (account: the account row the task's account, account_city name; lead: the lead row the task's lead_company name, the target row): account: stream_updated_at; lead: modified_at, modified_by_id, stream_updated_at. Changed on rows the generator can't identify, so a write that changes them stops: contact: stream_updated_at. Rows the app adds beside each write, expected exactly that many times: action_history_record (1 per write), auth_log_record (1 per write), call_user (1 per write), note (1 per write). Tables that gained a different number of rows in different demonstrations; they are not logs unless you list them under logs_add (rows may then be added, existing rows must not change): note, note_user. Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): account_contact: role; call: created_at, modified_at, modified_by_id, parent_type, uid; contact: address_city, address_country, address_postal_code, address_state, address_street, assigned_user_id, campaign_id, created_at, description, middle_name, modified_at, modified_by_id, salutation_name, stream_updated_at; opportunity: assigned_user_id, campaign_id, contact_id, created_at, description, last_stage, lead_source, modified_at, modified_by_id, probability, stream_updated_at. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log. - - Answer key: `bookkeeping`. Proposed: `accept`. - - Answered: `{"logs_add": ["action_history_record", "auth_log_record"]}` - -3. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. - - Answer key: `recheck_after_s`. Proposed: `300`. - - Answered: `300` - -## Kinds - -- `create_contact` (create, main table `contact`): 64 contract(s). -- `create_opportunity` (create, main table `opportunity`): 64 contract(s). -- `update_lead` (update, main table `lead`): 43 contract(s). -- `log_call` (create, main table `call`): 64 contract(s). diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml deleted file mode 100644 index 21499e1086..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/answers.yaml +++ /dev/null @@ -1,4 +0,0 @@ -phones: [create_contact.phone] -bookkeeping: - logs_add: [action_history_record, auth_log_record] -recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml deleted file mode 100644 index 0c52689923..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml +++ /dev/null @@ -1,400 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_contact, create, fields account, account_city, email, first_name, last_name, phone, title. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-espocrm -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - charset: utf8mb4 - database: espocrm - host: 127.0.0.1 - port: 9621 - user: deployer_ro - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` - AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` - AS CHAR) AS `last_name`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`do_not_call` - AS CHAR) AS `do_not_call`, CAST(t.`address_street` AS CHAR) AS `address_street`, CAST(t.`address_city` - AS CHAR) AS `address_city`, CAST(t.`address_state` AS CHAR) AS `address_state`, CAST(t.`address_country` - AS CHAR) AS `address_country`, CAST(t.`address_postal_code` AS CHAR) AS `address_postal_code`, - CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, - CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, - CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, - CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, - CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, (SELECT CASE WHEN (SELECT COUNT(*) FROM - `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) - AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM - `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, - 0) = 0) AS `p__account`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` - AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, - 0) = 0) = 1 THEN CAST(a1.`billing_address_city` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` - AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, - CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, CASE - WHEN t.`do_not_call` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__do_not_call` - FROM `contact` t - rows_account_contact: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`contact_id` - AS CHAR) AS `contact_id`, CAST(t.`role` AS CHAR) AS `role`, CAST(t.`is_inactive` AS CHAR) AS `is_inactive`, - CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` - u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND - CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM - `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, - 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS - `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` - AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, - 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` - AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` - a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, - (SELECT CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` - = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT - COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM - `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, - 0) = 0) AS `p__account`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' - END AS `pin__deleted`, CASE WHEN t.`is_inactive` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' - END AS `pin__is_inactive`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` - AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, - 0) = 0) = 1 THEN CAST(a1.`billing_address_city` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` - AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city` - FROM `account_contact` t - rows_entity_email_address: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` - AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` - AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT - COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM - `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, - 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS - `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` - AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, - 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` - AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` - a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, (SELECT - CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` - = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT - COUNT(*) FROM `email_address` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM - `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS BINARY) AND COALESCE(a1.`deleted`, - 0) = 0) AS `p__email`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `pin__primary` FROM `entity_email_address` t - rows_entity_phone_number: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`phone_number_id` - AS CHAR) AS `phone_number_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` - AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT - COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM - `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, - 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS - `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` - AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, - 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` - AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` - a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, (SELECT - CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` - = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT - COUNT(*) FROM `phone_number` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM - `phone_number` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`phone_number_id` AS BINARY) AND COALESCE(a1.`deleted`, - 0) = 0) AS `p__phone`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `pin__primary` FROM `entity_phone_number` t - raw_account: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` - AS CHAR) AS `deleted`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`type` AS CHAR) AS `type`, - CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`sic_code` AS CHAR) AS `sic_code`, CAST(t.`billing_address_street` - AS CHAR) AS `billing_address_street`, CAST(t.`billing_address_city` AS CHAR) AS `billing_address_city`, - CAST(t.`billing_address_state` AS CHAR) AS `billing_address_state`, CAST(t.`billing_address_country` - AS CHAR) AS `billing_address_country`, CAST(t.`billing_address_postal_code` AS CHAR) AS `billing_address_postal_code`, - CAST(t.`shipping_address_street` AS CHAR) AS `shipping_address_street`, CAST(t.`shipping_address_city` - AS CHAR) AS `shipping_address_city`, CAST(t.`shipping_address_state` AS CHAR) AS `shipping_address_state`, - CAST(t.`shipping_address_country` AS CHAR) AS `shipping_address_country`, CAST(t.`shipping_address_postal_code` - AS CHAR) AS `shipping_address_postal_code`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`is_locked` - AS CHAR) AS `is_locked`, CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS - CHAR) AS `modified_at`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`campaign_id` - AS CHAR) AS `campaign_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` - AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` - AS CHAR) AS `version_number` FROM `account` t - raw_email_address: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` - AS CHAR) AS `deleted`, CAST(t.`lower` AS CHAR) AS `lower`, CAST(t.`invalid` AS CHAR) AS `invalid`, - CAST(t.`opt_out` AS CHAR) AS `opt_out` FROM `email_address` t - raw_phone_number: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` - AS CHAR) AS `deleted`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`numeric` AS CHAR) AS `numeric`, - CAST(t.`invalid` AS CHAR) AS `invalid`, CAST(t.`opt_out` AS CHAR) AS `opt_out` FROM `phone_number` - t - raw_note: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`post` - AS CHAR) AS `post`, CAST(t.`data` AS CHAR) AS `data`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`target_type` - AS CHAR) AS `target_type`, CAST(t.`number` AS CHAR) AS `number`, CAST(t.`is_global` AS CHAR) AS - `is_global`, CAST(t.`is_internal` AS CHAR) AS `is_internal`, CAST(t.`is_pinned` AS CHAR) AS `is_pinned`, - CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, - CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` AS CHAR) AS `parent_type`, CAST(t.`related_id` - AS CHAR) AS `related_id`, CAST(t.`related_type` AS CHAR) AS `related_type`, CAST(t.`created_by_id` - AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`super_parent_id` - AS CHAR) AS `super_parent_id`, CAST(t.`super_parent_type` AS CHAR) AS `super_parent_type` FROM - `note` t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__account: - param: account - p__account_city: - param: account_city - first_name: - param: first_name - last_name: - param: last_name - created_by_id: - param: bot_user_id - pin__deleted: - literal: '1' - pin__do_not_call: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_account_contact|a row beside the main record holds account - match: - main__account: - param: account - main__account_city: - param: account_city - main__first_name: - param: first_name - main__last_name: - param: last_name - p__account: - param: account - pin__deleted: - literal: '1' - pin__is_inactive: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_account_contact|a row beside the main record holds account_city - match: - main__account: - param: account - main__account_city: - param: account_city - main__first_name: - param: first_name - main__last_name: - param: last_name - p__account_city: - param: account_city - pin__deleted: - literal: '1' - pin__is_inactive: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_entity_email_address|a row beside the main record holds email - match: - main__account: - param: account - main__account_city: - param: account_city - main__first_name: - param: first_name - main__last_name: - param: last_name - p__email: - param: email - kind: email - pin__deleted: - literal: '1' - entity_type: - literal: Contact - pin__primary: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_entity_phone_number|a row beside the main record holds phone - match: - main__account: - param: account - main__account_city: - param: account_city - main__first_name: - param: first_name - main__last_name: - param: last_name - p__phone: - param: phone - kind: phone - pin__deleted: - literal: '1' - entity_type: - literal: Contact - pin__primary: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_account_contact|a row beside the main record holds title - match: - main__account: - param: account - main__account_city: - param: account_city - main__first_name: - param: first_name - main__last_name: - param: last_name - role: - param: title - pin__deleted: - literal: '1' - pin__is_inactive: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_account|the account row the task's account, account_city name, changed by the app - match: - name: - param: account - billing_address_city: - param: account_city - expected_count: 1 -- kind: record_written - probe: surface=raw_email_address|the new email_address row holds email - match: - name: - param: email - kind: email - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_phone_number|the new phone_number row holds phone - match: - name: - param: phone - kind: phone - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_note|1 row(s) the app adds to note - match: - created_by_id: - param: bot_user_id - deleted: - literal: '0' - is_global: - literal: '0' - is_internal: - literal: '0' - is_pinned: - literal: '0' - parent_type: - literal: Contact - super_parent_type: - literal: Account - type: - literal: Create - expected_count: 1 - count_new_only: true -frame: - target_fields: - - stream_updated_at - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - allow_tables: - - table: action_history_record - - table: auth_log_record - - table: account_contact - - table: contact - - table: entity_email_address - - table: entity_phone_number - views: - account: raw_account - email_address: raw_email_address - phone_number: raw_phone_number - note: raw_note - settle_s: 1 - settle_attempts: 5 - new_row_fields: - email_address: - - deleted - - id - - invalid - - lower - - opt_out - phone_number: - - deleted - - id - - invalid - - numeric - - opt_out - - type - note: - - created_at - - data - - id - - modified_at - - modified_by_id - - number - - parent_id - - post - - related_id - - related_type - - super_parent_id - - target_type - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml deleted file mode 100644 index 3776794630..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml +++ /dev/null @@ -1,263 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_contact, found, fields account, account_city, email, first_name, last_name, phone, title. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-espocrm -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - charset: utf8mb4 - database: espocrm - host: 127.0.0.1 - port: 9621 - user: deployer_ro - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` - AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` - AS CHAR) AS `last_name`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`do_not_call` - AS CHAR) AS `do_not_call`, CAST(t.`address_street` AS CHAR) AS `address_street`, CAST(t.`address_city` - AS CHAR) AS `address_city`, CAST(t.`address_state` AS CHAR) AS `address_state`, CAST(t.`address_country` - AS CHAR) AS `address_country`, CAST(t.`address_postal_code` AS CHAR) AS `address_postal_code`, - CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, - CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, - CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, - CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, - CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, (SELECT CASE WHEN (SELECT COUNT(*) FROM - `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) - AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM - `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, - 0) = 0) AS `p__account`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` - AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, - 0) = 0) = 1 THEN CAST(a1.`billing_address_city` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` - AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, - CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, CASE - WHEN t.`do_not_call` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__do_not_call` - FROM `contact` t - rows_account_contact: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`contact_id` - AS CHAR) AS `contact_id`, CAST(t.`role` AS CHAR) AS `role`, CAST(t.`is_inactive` AS CHAR) AS `is_inactive`, - CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` - u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND - CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM - `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, - 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS - `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` - AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, - 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` - AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` - a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, - (SELECT CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`contact_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` - = t.`contact_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT - COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM - `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, - 0) = 0) AS `p__account`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' - END AS `pin__deleted`, CASE WHEN t.`is_inactive` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' - END AS `pin__is_inactive`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` - AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, - 0) = 0) = 1 THEN CAST(a1.`billing_address_city` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` - AS BINARY) = CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city` - FROM `account_contact` t - rows_entity_email_address: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` - AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` - AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT - COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM - `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, - 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS - `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` - AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, - 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` - AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` - a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, (SELECT - CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` - = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT - COUNT(*) FROM `email_address` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM - `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS BINARY) AND COALESCE(a1.`deleted`, - 0) = 0) AS `p__email`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `pin__primary` FROM `entity_email_address` t - rows_entity_phone_number: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`phone_number_id` - AS CHAR) AS `phone_number_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` - AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT (SELECT CASE WHEN (SELECT - COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM - `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, - 0) = 0) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS - `main__account`, (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` - AS CHAR) AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, - 0) = 0) = 1 THEN CAST(a2.`billing_address_city` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` - AS BINARY) = CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `contact` - a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account_city`, (SELECT - CAST(a1.`first_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `contact` a1 WHERE a1.`id` - = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT - COUNT(*) FROM `phone_number` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM - `phone_number` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`phone_number_id` AS BINARY) AND COALESCE(a1.`deleted`, - 0) = 0) AS `p__phone`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `pin__primary` FROM `entity_phone_number` t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__account: - param: account - p__account_city: - param: account_city - first_name: - param: first_name - last_name: - param: last_name - pin__deleted: - literal: '1' - pin__do_not_call: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=rows_account_contact|a row beside the main record holds account - match: - main__account: - param: account - main__account_city: - param: account_city - main__first_name: - param: first_name - main__last_name: - param: last_name - p__account: - param: account - pin__deleted: - literal: '1' - pin__is_inactive: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=rows_account_contact|a row beside the main record holds account_city - match: - main__account: - param: account - main__account_city: - param: account_city - main__first_name: - param: first_name - main__last_name: - param: last_name - p__account_city: - param: account_city - pin__deleted: - literal: '1' - pin__is_inactive: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=rows_entity_email_address|a row beside the main record holds email - match: - main__account: - param: account - main__account_city: - param: account_city - main__first_name: - param: first_name - main__last_name: - param: last_name - p__email: - param: email - kind: email - pin__deleted: - literal: '1' - entity_type: - literal: Contact - pin__primary: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=rows_entity_phone_number|a row beside the main record holds phone - match: - main__account: - param: account - main__account_city: - param: account_city - main__first_name: - param: first_name - main__last_name: - param: last_name - p__phone: - param: phone - kind: phone - pin__deleted: - literal: '1' - entity_type: - literal: Contact - pin__primary: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=rows_account_contact|a row beside the main record holds title - match: - main__account: - param: account - main__account_city: - param: account_city - main__first_name: - param: first_name - main__last_name: - param: last_name - role: - param: title - pin__deleted: - literal: '1' - pin__is_inactive: - literal: '1' - expected_count: 1 -frame: - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - allow_tables: - - table: action_history_record - - table: auth_log_record - - table: account_contact - - table: contact - - table: entity_email_address - - table: entity_phone_number - views: {} - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml deleted file mode 100644 index 03417572ff..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml +++ /dev/null @@ -1,171 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_opportunity, create, fields account, account_city, amount, close_date, currency, name, stage. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-espocrm -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - charset: utf8mb4 - database: espocrm - host: 127.0.0.1 - port: 9621 - user: deployer_ro - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` - AS CHAR) AS `deleted`, CAST(t.`amount` AS CHAR) AS `amount`, CAST(t.`stage` AS CHAR) AS `stage`, - CAST(t.`last_stage` AS CHAR) AS `last_stage`, CAST(t.`probability` AS CHAR) AS `probability`, - CAST(t.`lead_source` AS CHAR) AS `lead_source`, CAST(t.`close_date` AS CHAR) AS `close_date`, - CAST(t.`description` AS CHAR) AS `description`, CAST(t.`created_at` AS CHAR) AS `created_at`, - CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`amount_currency` AS CHAR) AS `amount_currency`, - CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`account_id` AS CHAR) AS `account_id`, - CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, - CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, - CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` AS CHAR) AS - `version_number`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` - AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, - 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = - CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account`, (SELECT CASE - WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`billing_address_city` - AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) - AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP - '^-?[0-9]+([.][0-9]+)?$' AND t.`amount` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `p__amount`, CASE WHEN t.`close_date` = CAST(%(v1)s AS DATE) THEN '1' ELSE '0' END AS `p__close_date`, - CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted`, CASE - WHEN t.`version_number` = (SELECT s.`version_number` FROM `account` s WHERE s.`id` = t.`account_id`) - THEN '1' ELSE '0' END AS `pin__version_number` FROM `opportunity` t - sql_query_params: - v0: - param: amount - v1: - param: close_date - raw_account: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` - AS CHAR) AS `deleted`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`type` AS CHAR) AS `type`, - CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`sic_code` AS CHAR) AS `sic_code`, CAST(t.`billing_address_street` - AS CHAR) AS `billing_address_street`, CAST(t.`billing_address_city` AS CHAR) AS `billing_address_city`, - CAST(t.`billing_address_state` AS CHAR) AS `billing_address_state`, CAST(t.`billing_address_country` - AS CHAR) AS `billing_address_country`, CAST(t.`billing_address_postal_code` AS CHAR) AS `billing_address_postal_code`, - CAST(t.`shipping_address_street` AS CHAR) AS `shipping_address_street`, CAST(t.`shipping_address_city` - AS CHAR) AS `shipping_address_city`, CAST(t.`shipping_address_state` AS CHAR) AS `shipping_address_state`, - CAST(t.`shipping_address_country` AS CHAR) AS `shipping_address_country`, CAST(t.`shipping_address_postal_code` - AS CHAR) AS `shipping_address_postal_code`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`is_locked` - AS CHAR) AS `is_locked`, CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS - CHAR) AS `modified_at`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`campaign_id` - AS CHAR) AS `campaign_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` - AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` - AS CHAR) AS `version_number` FROM `account` t - raw_note: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`post` - AS CHAR) AS `post`, CAST(t.`data` AS CHAR) AS `data`, CAST(t.`type` AS CHAR) AS `type`, CAST(t.`target_type` - AS CHAR) AS `target_type`, CAST(t.`number` AS CHAR) AS `number`, CAST(t.`is_global` AS CHAR) AS - `is_global`, CAST(t.`is_internal` AS CHAR) AS `is_internal`, CAST(t.`is_pinned` AS CHAR) AS `is_pinned`, - CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, - CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` AS CHAR) AS `parent_type`, CAST(t.`related_id` - AS CHAR) AS `related_id`, CAST(t.`related_type` AS CHAR) AS `related_type`, CAST(t.`created_by_id` - AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`super_parent_id` - AS CHAR) AS `super_parent_id`, CAST(t.`super_parent_type` AS CHAR) AS `super_parent_type` FROM - `note` t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__account: - param: account - p__account_city: - param: account_city - p__amount: - literal: '1' - p__close_date: - literal: '1' - amount_currency: - param: currency - name: - param: name - stage: - param: stage - created_by_id: - param: bot_user_id - pin__deleted: - literal: '1' - pin__version_number: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_account|the account row the task's account, account_city name, changed by the app - match: - name: - param: account - billing_address_city: - param: account_city - expected_count: 1 -- kind: record_written - probe: surface=raw_note|1 row(s) the app adds to note - match: - created_by_id: - param: bot_user_id - deleted: - literal: '0' - is_global: - literal: '0' - is_internal: - literal: '0' - is_pinned: - literal: '0' - parent_type: - literal: Opportunity - super_parent_type: - literal: Account - type: - literal: Create - expected_count: 1 - count_new_only: true -frame: - target_fields: - - stream_updated_at - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - allow_tables: - - table: action_history_record - - table: auth_log_record - - table: opportunity - views: - account: raw_account - note: raw_note - settle_s: 1 - settle_attempts: 5 - new_row_fields: - note: - - created_at - - data - - id - - modified_at - - modified_by_id - - number - - parent_id - - post - - related_id - - related_type - - super_parent_id - - target_type - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml deleted file mode 100644 index f61d477b2a..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml +++ /dev/null @@ -1,84 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_opportunity, found, fields account, account_city, amount, close_date, currency, name, stage. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-espocrm -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - charset: utf8mb4 - database: espocrm - host: 127.0.0.1 - port: 9621 - user: deployer_ro - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` - AS CHAR) AS `deleted`, CAST(t.`amount` AS CHAR) AS `amount`, CAST(t.`stage` AS CHAR) AS `stage`, - CAST(t.`last_stage` AS CHAR) AS `last_stage`, CAST(t.`probability` AS CHAR) AS `probability`, - CAST(t.`lead_source` AS CHAR) AS `lead_source`, CAST(t.`close_date` AS CHAR) AS `close_date`, - CAST(t.`description` AS CHAR) AS `description`, CAST(t.`created_at` AS CHAR) AS `created_at`, - CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`amount_currency` AS CHAR) AS `amount_currency`, - CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`account_id` AS CHAR) AS `account_id`, - CAST(t.`contact_id` AS CHAR) AS `contact_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, - CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, - CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` AS CHAR) AS - `version_number`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` - AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, - 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = - CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account`, (SELECT CASE - WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`billing_address_city` - AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) - AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, CASE WHEN CAST(%(v0)s AS CHAR) REGEXP - '^-?[0-9]+([.][0-9]+)?$' AND t.`amount` = CAST(%(v0)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `p__amount`, CASE WHEN t.`close_date` = CAST(%(v1)s AS DATE) THEN '1' ELSE '0' END AS `p__close_date`, - CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted` FROM - `opportunity` t - sql_query_params: - v0: - param: amount - v1: - param: close_date -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__account: - param: account - p__account_city: - param: account_city - p__amount: - literal: '1' - p__close_date: - literal: '1' - amount_currency: - param: currency - name: - param: name - stage: - param: stage - pin__deleted: - literal: '1' - expected_count: 1 -frame: - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - allow_tables: - - table: action_history_record - - table: auth_log_record - - table: opportunity - views: {} - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml deleted file mode 100644 index c6a4812965..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml +++ /dev/null @@ -1,210 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind log_call, create, fields account, account_city, contact, date_start, description, direction, duration_minutes, status, subject. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-espocrm -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - charset: utf8mb4 - database: espocrm - host: 127.0.0.1 - port: 9621 - user: deployer_ro - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` - AS CHAR) AS `deleted`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`date_start` AS CHAR) AS `date_start`, - CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`direction` AS CHAR) AS `direction`, CAST(t.`description` - AS CHAR) AS `description`, CAST(t.`uid` AS CHAR) AS `uid`, CAST(t.`created_at` AS CHAR) AS `created_at`, - CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` - AS CHAR) AS `parent_type`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`created_by_id` - AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` - AS CHAR) AS `assigned_user_id`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` - AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, - 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = - CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account`, (SELECT CASE - WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`billing_address_city` - AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) - AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, CASE WHEN t.`date_start` = CAST(%(v0)s - AS DATETIME) THEN '1' ELSE '0' END AS `p__date_start`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND (TIMESTAMPDIFF(SECOND, t.`date_start`, t.`date_end`) / 60) = CAST(%(v1)s AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `p__duration_minutes`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__deleted` FROM `call` t - sql_query_params: - v0: &id002 - param: date_start - v1: &id003 - param: duration_minutes - rows_call_contact: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`call_id` AS CHAR) AS `call_id`, CAST(t.`contact_id` - AS CHAR) AS `contact_id`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`deleted` AS CHAR) AS `deleted`, - (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) - AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS - CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, - 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = - CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `call` a1 WHERE a1.`id` - = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account`, (SELECT (SELECT CASE WHEN - (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`billing_address_city` - AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) - AND COALESCE(a2.`deleted`, 0) = 0) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__account_city`, CASE WHEN (SELECT a1.`date_start` FROM `call` a1 WHERE a1.`id` - = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' - END AS `main__date_start`, (SELECT CAST(a1.`description` AS CHAR) FROM `call` a1 WHERE a1.`id` - = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__description`, (SELECT CAST(a1.`direction` - AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__direction`, - CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT (TIMESTAMPDIFF(SECOND, - a1.`date_start`, a1.`date_end`) / 60) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, - 0) = 0) = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__duration_minutes`, (SELECT - CAST(a1.`status` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__status`, (SELECT CAST(a1.`name` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` - AND COALESCE(a1.`deleted`, 0) = 0) AS `main__subject`, (SELECT CASE WHEN (SELECT COUNT(*) FROM - `contact` u1 WHERE CAST(CONCAT(CAST(u1.`first_name` AS CHAR), ' ', CAST(u1.`last_name` AS CHAR)) - AS BINARY) = CAST(CONCAT(CAST(a1.`first_name` AS CHAR), ' ', CAST(a1.`last_name` AS CHAR)) AS - BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CONCAT(CAST(a1.`first_name` AS CHAR), ' ', - CAST(a1.`last_name` AS CHAR)) END FROM `contact` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`contact_id` - AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__contact`, CASE WHEN t.`deleted` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted` FROM `call_contact` t - sql_query_params: - v0: *id002 - v1: *id003 - raw_account: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` - AS CHAR) AS `deleted`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`type` AS CHAR) AS `type`, - CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`sic_code` AS CHAR) AS `sic_code`, CAST(t.`billing_address_street` - AS CHAR) AS `billing_address_street`, CAST(t.`billing_address_city` AS CHAR) AS `billing_address_city`, - CAST(t.`billing_address_state` AS CHAR) AS `billing_address_state`, CAST(t.`billing_address_country` - AS CHAR) AS `billing_address_country`, CAST(t.`billing_address_postal_code` AS CHAR) AS `billing_address_postal_code`, - CAST(t.`shipping_address_street` AS CHAR) AS `shipping_address_street`, CAST(t.`shipping_address_city` - AS CHAR) AS `shipping_address_city`, CAST(t.`shipping_address_state` AS CHAR) AS `shipping_address_state`, - CAST(t.`shipping_address_country` AS CHAR) AS `shipping_address_country`, CAST(t.`shipping_address_postal_code` - AS CHAR) AS `shipping_address_postal_code`, CAST(t.`description` AS CHAR) AS `description`, CAST(t.`is_locked` - AS CHAR) AS `is_locked`, CAST(t.`created_at` AS CHAR) AS `created_at`, CAST(t.`modified_at` AS - CHAR) AS `modified_at`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, CAST(t.`campaign_id` - AS CHAR) AS `campaign_id`, CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` - AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`version_number` - AS CHAR) AS `version_number` FROM `account` t - raw_call_user: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`user_id` AS CHAR) AS `user_id`, CAST(t.`call_id` - AS CHAR) AS `call_id`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`deleted` AS CHAR) AS `deleted` - FROM `call_user` t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__account: - param: account - p__account_city: - param: account_city - p__date_start: - literal: '1' - description: - param: description - direction: - param: direction - p__duration_minutes: - literal: '1' - status: - param: status - name: - param: subject - assigned_user_id: - param: bot_user_id - created_by_id: - param: bot_user_id - pin__deleted: - literal: '1' - parent_type: - literal: Account - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_call_contact|a row beside the main record holds contact - match: - main__account: - param: account - main__account_city: - param: account_city - main__date_start: - literal: '1' - main__description: - param: description - main__direction: - param: direction - main__duration_minutes: - literal: '1' - main__status: - param: status - main__subject: - param: subject - p__contact: - param: contact - pin__deleted: - literal: '1' - status: - literal: None - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_account|the account row the task's account, account_city name, changed by the app - match: - name: - param: account - billing_address_city: - param: account_city - expected_count: 1 -- kind: record_written - probe: surface=raw_call_user|1 row(s) the app adds to call_user - match: - deleted: - literal: '0' - status: - literal: Accepted - user_id: - param: bot_user_id - expected_count: 1 - count_new_only: true -frame: - target_fields: - - stream_updated_at - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - allow_tables: - - table: action_history_record - - table: auth_log_record - - table: call - - table: call_contact - views: - account: raw_account - call_user: raw_call_user - settle_s: 1 - settle_attempts: 5 - new_row_fields: - call_user: - - call_id - - id - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml deleted file mode 100644 index 5fa761951a..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml +++ /dev/null @@ -1,150 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind log_call, found, fields account, account_city, contact, date_start, description, direction, duration_minutes, status, subject. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-espocrm -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - charset: utf8mb4 - database: espocrm - host: 127.0.0.1 - port: 9621 - user: deployer_ro - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` - AS CHAR) AS `deleted`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`date_start` AS CHAR) AS `date_start`, - CAST(t.`date_end` AS CHAR) AS `date_end`, CAST(t.`direction` AS CHAR) AS `direction`, CAST(t.`description` - AS CHAR) AS `description`, CAST(t.`uid` AS CHAR) AS `uid`, CAST(t.`created_at` AS CHAR) AS `created_at`, - CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`parent_id` AS CHAR) AS `parent_id`, CAST(t.`parent_type` - AS CHAR) AS `parent_type`, CAST(t.`account_id` AS CHAR) AS `account_id`, CAST(t.`created_by_id` - AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, CAST(t.`assigned_user_id` - AS CHAR) AS `assigned_user_id`, (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` - AS CHAR) AS BINARY) = CAST(CAST(a1.`name` AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` - AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, - 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = - CAST(t.`account_id` AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account`, (SELECT CASE - WHEN (SELECT COUNT(*) FROM `account` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u1.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a1.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`billing_address_city` - AS CHAR) END FROM `account` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`account_id` AS BINARY) - AND COALESCE(a1.`deleted`, 0) = 0) AS `p__account_city`, CASE WHEN t.`date_start` = CAST(%(v0)s - AS DATETIME) THEN '1' ELSE '0' END AS `p__date_start`, CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' - AND (TIMESTAMPDIFF(SECOND, t.`date_start`, t.`date_end`) / 60) = CAST(%(v1)s AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `p__duration_minutes`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) - THEN '1' ELSE '0' END AS `pin__deleted` FROM `call` t - sql_query_params: - v0: &id002 - param: date_start - v1: &id003 - param: duration_minutes - rows_call_contact: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`call_id` AS CHAR) AS `call_id`, CAST(t.`contact_id` - AS CHAR) AS `contact_id`, CAST(t.`status` AS CHAR) AS `status`, CAST(t.`deleted` AS CHAR) AS `deleted`, - (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) - AS BINARY) = CAST(CAST(a2.`name` AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS - CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, - 0) = 0) = 1 THEN CAST(a2.`name` AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = - CAST(a1.`account_id` AS BINARY) AND COALESCE(a2.`deleted`, 0) = 0) FROM `call` a1 WHERE a1.`id` - = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__account`, (SELECT (SELECT CASE WHEN - (SELECT COUNT(*) FROM `account` u2 WHERE CAST(CAST(u2.`name` AS CHAR) AS BINARY) = CAST(CAST(a2.`name` - AS CHAR) AS BINARY) AND CAST(CAST(u2.`billing_address_city` AS CHAR) AS BINARY) = CAST(CAST(a2.`billing_address_city` - AS CHAR) AS BINARY) AND COALESCE(u2.`deleted`, 0) = 0) = 1 THEN CAST(a2.`billing_address_city` - AS CHAR) END FROM `account` a2 WHERE CAST(a2.`id` AS BINARY) = CAST(a1.`account_id` AS BINARY) - AND COALESCE(a2.`deleted`, 0) = 0) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__account_city`, CASE WHEN (SELECT a1.`date_start` FROM `call` a1 WHERE a1.`id` - = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) = CAST(%(v0)s AS DATETIME) THEN '1' ELSE '0' - END AS `main__date_start`, (SELECT CAST(a1.`description` AS CHAR) FROM `call` a1 WHERE a1.`id` - = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__description`, (SELECT CAST(a1.`direction` - AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__direction`, - CASE WHEN CAST(%(v1)s AS CHAR) REGEXP '^-?[0-9]+([.][0-9]+)?$' AND (SELECT (TIMESTAMPDIFF(SECOND, - a1.`date_start`, a1.`date_end`) / 60) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, - 0) = 0) = CAST(%(v1)s AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `main__duration_minutes`, (SELECT - CAST(a1.`status` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__status`, (SELECT CAST(a1.`name` AS CHAR) FROM `call` a1 WHERE a1.`id` = t.`call_id` - AND COALESCE(a1.`deleted`, 0) = 0) AS `main__subject`, (SELECT CASE WHEN (SELECT COUNT(*) FROM - `contact` u1 WHERE CAST(CONCAT(CAST(u1.`first_name` AS CHAR), ' ', CAST(u1.`last_name` AS CHAR)) - AS BINARY) = CAST(CONCAT(CAST(a1.`first_name` AS CHAR), ' ', CAST(a1.`last_name` AS CHAR)) AS - BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CONCAT(CAST(a1.`first_name` AS CHAR), ' ', - CAST(a1.`last_name` AS CHAR)) END FROM `contact` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`contact_id` - AS BINARY) AND COALESCE(a1.`deleted`, 0) = 0) AS `p__contact`, CASE WHEN t.`deleted` = CAST('0' - AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS `pin__deleted` FROM `call_contact` t - sql_query_params: - v0: *id002 - v1: *id003 -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__account: - param: account - p__account_city: - param: account_city - p__date_start: - literal: '1' - description: - param: description - direction: - param: direction - p__duration_minutes: - literal: '1' - status: - param: status - name: - param: subject - pin__deleted: - literal: '1' - parent_type: - literal: Account - expected_count: 1 -- kind: record_written - probe: surface=rows_call_contact|a row beside the main record holds contact - match: - main__account: - param: account - main__account_city: - param: account_city - main__date_start: - literal: '1' - main__description: - param: description - main__direction: - param: direction - main__duration_minutes: - literal: '1' - main__status: - param: status - main__subject: - param: subject - p__contact: - param: contact - pin__deleted: - literal: '1' - status: - literal: None - expected_count: 1 -frame: - target_fields: [] - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - allow_tables: - - table: action_history_record - - table: auth_log_record - - table: call - - table: call_contact - views: {} - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml deleted file mode 100644 index a9b5932b78..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml +++ /dev/null @@ -1,115 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind update_lead, found, fields company, field, first_name, last_name, value. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-espocrm -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - charset: utf8mb4 - database: espocrm - host: 127.0.0.1 - port: 9621 - user: deployer_ro - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` - AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` - AS CHAR) AS `last_name`, CAST(t.`title` AS CHAR) AS `title`, CAST(t.`status` AS CHAR) AS `status`, - CAST(t.`source` AS CHAR) AS `source`, CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`opportunity_amount` - AS CHAR) AS `opportunity_amount`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`address_street` - AS CHAR) AS `address_street`, CAST(t.`address_city` AS CHAR) AS `address_city`, CAST(t.`address_state` - AS CHAR) AS `address_state`, CAST(t.`address_country` AS CHAR) AS `address_country`, CAST(t.`address_postal_code` - AS CHAR) AS `address_postal_code`, CAST(t.`do_not_call` AS CHAR) AS `do_not_call`, CAST(t.`description` - AS CHAR) AS `description`, CAST(t.`converted_at` AS CHAR) AS `converted_at`, CAST(t.`created_at` - AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`account_name` - AS CHAR) AS `account_name`, CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`opportunity_amount_currency` - AS CHAR) AS `opportunity_amount_currency`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, - CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, - CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, - CAST(t.`created_account_id` AS CHAR) AS `created_account_id`, CAST(t.`created_contact_id` AS CHAR) - AS `created_contact_id`, CAST(t.`created_opportunity_id` AS CHAR) AS `created_opportunity_id` - FROM `lead` t - rows_entity_email_address: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` - AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` - AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT CAST(a1.`account_name` - AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__company`, - (SELECT CAST(a1.`first_name` AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `lead` a1 WHERE a1.`id` - = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT - COUNT(*) FROM `email_address` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM - `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS BINARY) AND COALESCE(a1.`deleted`, - 0) = 0) AS `p__value`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `pin__primary` FROM `entity_email_address` t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - account_name: - param: company - first_name: - param: first_name - last_name: - param: last_name - expected_count: 1 -- kind: record_written - probe: surface=rows_entity_email_address|a row beside the main record holds value - match: - main__company: - param: company - main__first_name: - param: first_name - main__last_name: - param: last_name - p__value: - param: value - kind: email - pin__deleted: - literal: '1' - entity_type: - literal: Lead - pin__primary: - literal: '1' - expected_count: 1 -frame: - target_fields: [] - versions: - read: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`target_id` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`user_id` - AS CHAR) AS `author` FROM `action_history_record` t WHERE CAST(t.`action` AS CHAR) = 'update' - AND CAST(t.`target_type` AS CHAR) = 'Lead' - record_field: id - target_field: id - version_field: ver - author_field: author - self: - - param: bot_user_id - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - allow_tables: - - table: action_history_record - - table: auth_log_record - - table: entity_email_address - - table: lead - views: {} - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml deleted file mode 100644 index d2d679b8dd..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml +++ /dev/null @@ -1,157 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind update_lead, update, fields company, field, first_name, last_name, value. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-espocrm -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: pymysql - sql_connect_args: &id001 - charset: utf8mb4 - database: espocrm - host: 127.0.0.1 - port: 9621 - user: deployer_ro - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`deleted` AS CHAR) AS `deleted`, CAST(t.`salutation_name` - AS CHAR) AS `salutation_name`, CAST(t.`first_name` AS CHAR) AS `first_name`, CAST(t.`last_name` - AS CHAR) AS `last_name`, CAST(t.`title` AS CHAR) AS `title`, CAST(t.`status` AS CHAR) AS `status`, - CAST(t.`source` AS CHAR) AS `source`, CAST(t.`industry` AS CHAR) AS `industry`, CAST(t.`opportunity_amount` - AS CHAR) AS `opportunity_amount`, CAST(t.`website` AS CHAR) AS `website`, CAST(t.`address_street` - AS CHAR) AS `address_street`, CAST(t.`address_city` AS CHAR) AS `address_city`, CAST(t.`address_state` - AS CHAR) AS `address_state`, CAST(t.`address_country` AS CHAR) AS `address_country`, CAST(t.`address_postal_code` - AS CHAR) AS `address_postal_code`, CAST(t.`do_not_call` AS CHAR) AS `do_not_call`, CAST(t.`description` - AS CHAR) AS `description`, CAST(t.`converted_at` AS CHAR) AS `converted_at`, CAST(t.`created_at` - AS CHAR) AS `created_at`, CAST(t.`modified_at` AS CHAR) AS `modified_at`, CAST(t.`account_name` - AS CHAR) AS `account_name`, CAST(t.`middle_name` AS CHAR) AS `middle_name`, CAST(t.`opportunity_amount_currency` - AS CHAR) AS `opportunity_amount_currency`, CAST(t.`stream_updated_at` AS CHAR) AS `stream_updated_at`, - CAST(t.`created_by_id` AS CHAR) AS `created_by_id`, CAST(t.`modified_by_id` AS CHAR) AS `modified_by_id`, - CAST(t.`assigned_user_id` AS CHAR) AS `assigned_user_id`, CAST(t.`campaign_id` AS CHAR) AS `campaign_id`, - CAST(t.`created_account_id` AS CHAR) AS `created_account_id`, CAST(t.`created_contact_id` AS CHAR) - AS `created_contact_id`, CAST(t.`created_opportunity_id` AS CHAR) AS `created_opportunity_id` - FROM `lead` t - rows_entity_email_address: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`entity_id` AS CHAR) AS `entity_id`, CAST(t.`email_address_id` - AS CHAR) AS `email_address_id`, CAST(t.`entity_type` AS CHAR) AS `entity_type`, CAST(t.`primary` - AS CHAR) AS `primary`, CAST(t.`deleted` AS CHAR) AS `deleted`, (SELECT CAST(a1.`account_name` - AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__company`, - (SELECT CAST(a1.`first_name` AS CHAR) FROM `lead` a1 WHERE a1.`id` = t.`entity_id` AND COALESCE(a1.`deleted`, - 0) = 0) AS `main__first_name`, (SELECT CAST(a1.`last_name` AS CHAR) FROM `lead` a1 WHERE a1.`id` - = t.`entity_id` AND COALESCE(a1.`deleted`, 0) = 0) AS `main__last_name`, (SELECT CASE WHEN (SELECT - COUNT(*) FROM `email_address` u1 WHERE CAST(CAST(u1.`name` AS CHAR) AS BINARY) = CAST(CAST(a1.`name` - AS CHAR) AS BINARY) AND COALESCE(u1.`deleted`, 0) = 0) = 1 THEN CAST(a1.`name` AS CHAR) END FROM - `email_address` a1 WHERE CAST(a1.`id` AS BINARY) = CAST(t.`email_address_id` AS BINARY) AND COALESCE(a1.`deleted`, - 0) = 0) AS `p__value`, CASE WHEN t.`deleted` = CAST('0' AS DECIMAL(65,30)) THEN '1' ELSE '0' END - AS `pin__deleted`, CASE WHEN t.`primary` = CAST('1' AS DECIMAL(65,30)) THEN '1' ELSE '0' END AS - `pin__primary` FROM `entity_email_address` t - raw_email_address: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`id` AS CHAR) AS `id`, CAST(t.`name` AS CHAR) AS `name`, CAST(t.`deleted` - AS CHAR) AS `deleted`, CAST(t.`lower` AS CHAR) AS `lower`, CAST(t.`invalid` AS CHAR) AS `invalid`, - CAST(t.`opt_out` AS CHAR) AS `opt_out` FROM `email_address` t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - account_name: - param: company - first_name: - param: first_name - last_name: - param: last_name - expected_count: 1 -- kind: record_written - probe: surface=rows_entity_email_address|a row beside the main record holds value - match: - main__company: &id002 - param: company - main__first_name: &id003 - param: first_name - main__last_name: &id004 - param: last_name - p__value: - param: value - kind: email - pin__deleted: &id005 - literal: '1' - entity_type: &id006 - literal: Lead - pin__primary: &id007 - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_entity_email_address|the record's rows here after the write - match: - main__company: *id002 - main__first_name: *id003 - main__last_name: *id004 - pin__deleted: *id005 - entity_type: *id006 - pin__primary: *id007 - expected_count: 1 -- kind: record_written - probe: surface=raw_email_address|the new email_address row holds value - match: - name: - param: value - kind: email - expected_count: 1 - count_new_only: true -frame: - target_fields: - - modified_at - - modified_by_id - - stream_updated_at - versions: - read: - kind: sql - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - sql_query: SELECT CAST(t.`target_id` AS CHAR) AS `id`, CAST(t.`id` AS CHAR) AS `ver`, CAST(t.`user_id` - AS CHAR) AS `author` FROM `action_history_record` t WHERE CAST(t.`action` AS CHAR) = 'update' - AND CAST(t.`target_type` AS CHAR) = 'Lead' - record_field: id - target_field: id - version_field: ver - author_field: author - self: - - param: bot_user_id - database: - dialect: mysql - read: - sql_driver: pymysql - sql_connect_args: *id001 - sql_password_env: ESPOCRM_DB_RO_PASSWORD - allow_tables: - - table: action_history_record - - table: auth_log_record - - table: entity_email_address - mode: skip - reason: read in full, every column, by this contract's own surface - - table: lead - mode: skip - reason: read in full, every column, by this contract's own surface - views: - email_address: raw_email_address - settle_s: 1 - settle_attempts: 5 - new_row_fields: - email_address: - - deleted - - id - - invalid - - lower - - opt_out - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml deleted file mode 100644 index 5af4af6bac..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/manifest.yaml +++ /dev/null @@ -1,2457 +0,0 @@ -schema_version: openadapt.contract-manifest/v1 -dialect: mysql -database: espocrm -actor_params: -- bot_user_id -questions_open: [] -kinds: - create_contact: - mode: create - main_table: contact - parameters: - - account - - account_city - - first_name - - last_name - - email - - phone - - title - identity: [] - lists: [] - dispatch: null - variants: - - fields: - - account - - account_city - - email - - first_name - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml - found: contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml - - fields: - - account - - account_city - - email - - first_name - - last_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+first_name+last_name.cc617045.create.yaml - found: contracts/create_contact/account+account_city+email+first_name+last_name.cc617045.found.yaml - - fields: - - account_city - - email - - first_name - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+first_name+last_name+phone+title.be4b0e8f.create.yaml - found: contracts/create_contact/account_city+email+first_name+last_name+phone+title.be4b0e8f.found.yaml - - fields: - - account - - email - - first_name - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+email+first_name+last_name+phone+title.ce919501.create.yaml - found: contracts/create_contact/account+email+first_name+last_name+phone+title.ce919501.found.yaml - - fields: - - account - - account_city - - first_name - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+first_name+last_name+phone+title.4ff4a105.create.yaml - found: contracts/create_contact/account+account_city+first_name+last_name+phone+title.4ff4a105.found.yaml - - fields: - - account - - account_city - - email - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+last_name+phone+title.41e87667.create.yaml - found: contracts/create_contact/account+account_city+email+last_name+phone+title.41e87667.found.yaml - - fields: - - account - - account_city - - email - - first_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+first_name+phone+title.308596e1.create.yaml - found: contracts/create_contact/account+account_city+email+first_name+phone+title.308596e1.found.yaml - - fields: - - account - - account_city - - email - - first_name - - last_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+first_name+last_name+title.ef6b5a09.create.yaml - found: contracts/create_contact/account+account_city+email+first_name+last_name+title.ef6b5a09.found.yaml - - fields: - - account - - account_city - - email - - first_name - - last_name - - phone - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+first_name+last_name+phone.66680a4a.create.yaml - found: contracts/create_contact/account+account_city+email+first_name+last_name+phone.66680a4a.found.yaml - - fields: - - account_city - - email - - first_name - - last_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+first_name+last_name.e363a157.create.yaml - found: contracts/create_contact/account_city+email+first_name+last_name.e363a157.found.yaml - - fields: - - account - - email - - first_name - - last_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account+email+first_name+last_name.8a4c37a1.create.yaml - found: contracts/create_contact/account+email+first_name+last_name.8a4c37a1.found.yaml - - fields: - - account - - account_city - - first_name - - last_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+first_name+last_name.0890bdcf.create.yaml - found: contracts/create_contact/account+account_city+first_name+last_name.0890bdcf.found.yaml - - fields: - - account - - account_city - - email - - last_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+last_name.0663f8b5.create.yaml - found: contracts/create_contact/account+account_city+email+last_name.0663f8b5.found.yaml - - fields: - - account - - account_city - - email - - first_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+first_name.73be94d9.create.yaml - found: contracts/create_contact/account+account_city+email+first_name.73be94d9.found.yaml - - fields: - - email - - first_name - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/email+first_name+last_name+phone+title.b657aeb5.create.yaml - found: contracts/create_contact/email+first_name+last_name+phone+title.b657aeb5.found.yaml - - fields: - - account_city - - first_name - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+first_name+last_name+phone+title.dc8ca344.create.yaml - found: contracts/create_contact/account_city+first_name+last_name+phone+title.dc8ca344.found.yaml - - fields: - - account_city - - email - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+last_name+phone+title.70958d35.create.yaml - found: contracts/create_contact/account_city+email+last_name+phone+title.70958d35.found.yaml - - fields: - - account_city - - email - - first_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+first_name+phone+title.bcc47205.create.yaml - found: contracts/create_contact/account_city+email+first_name+phone+title.bcc47205.found.yaml - - fields: - - account_city - - email - - first_name - - last_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+first_name+last_name+title.4b29e4b5.create.yaml - found: contracts/create_contact/account_city+email+first_name+last_name+title.4b29e4b5.found.yaml - - fields: - - account_city - - email - - first_name - - last_name - - phone - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+first_name+last_name+phone.7fc1c0d6.create.yaml - found: contracts/create_contact/account_city+email+first_name+last_name+phone.7fc1c0d6.found.yaml - - fields: - - account - - first_name - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+first_name+last_name+phone+title.355dafc1.create.yaml - found: contracts/create_contact/account+first_name+last_name+phone+title.355dafc1.found.yaml - - fields: - - account - - email - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+email+last_name+phone+title.a4c2b999.create.yaml - found: contracts/create_contact/account+email+last_name+phone+title.a4c2b999.found.yaml - - fields: - - account - - email - - first_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+email+first_name+phone+title.8cf06d5a.create.yaml - found: contracts/create_contact/account+email+first_name+phone+title.8cf06d5a.found.yaml - - fields: - - account - - email - - first_name - - last_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+email+first_name+last_name+title.e9c71bcd.create.yaml - found: contracts/create_contact/account+email+first_name+last_name+title.e9c71bcd.found.yaml - - fields: - - account - - email - - first_name - - last_name - - phone - lists: {} - dispatch_value: null - create: contracts/create_contact/account+email+first_name+last_name+phone.77aad286.create.yaml - found: contracts/create_contact/account+email+first_name+last_name+phone.77aad286.found.yaml - - fields: - - account - - account_city - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+last_name+phone+title.0f22d92a.create.yaml - found: contracts/create_contact/account+account_city+last_name+phone+title.0f22d92a.found.yaml - - fields: - - account - - account_city - - first_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+first_name+phone+title.9c11906f.create.yaml - found: contracts/create_contact/account+account_city+first_name+phone+title.9c11906f.found.yaml - - fields: - - account - - account_city - - first_name - - last_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+first_name+last_name+title.e278e40a.create.yaml - found: contracts/create_contact/account+account_city+first_name+last_name+title.e278e40a.found.yaml - - fields: - - account - - account_city - - first_name - - last_name - - phone - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+first_name+last_name+phone.29f540dc.create.yaml - found: contracts/create_contact/account+account_city+first_name+last_name+phone.29f540dc.found.yaml - - fields: - - account - - account_city - - email - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+phone+title.96539c9c.create.yaml - found: contracts/create_contact/account+account_city+email+phone+title.96539c9c.found.yaml - - fields: - - account - - account_city - - email - - last_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+last_name+title.58914f1d.create.yaml - found: contracts/create_contact/account+account_city+email+last_name+title.58914f1d.found.yaml - - fields: - - account - - account_city - - email - - last_name - - phone - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+last_name+phone.dc15be7c.create.yaml - found: contracts/create_contact/account+account_city+email+last_name+phone.dc15be7c.found.yaml - - fields: - - account - - account_city - - email - - first_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+first_name+title.bf9dcd88.create.yaml - found: contracts/create_contact/account+account_city+email+first_name+title.bf9dcd88.found.yaml - - fields: - - account - - account_city - - email - - first_name - - phone - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email+first_name+phone.01d2568c.create.yaml - found: contracts/create_contact/account+account_city+email+first_name+phone.01d2568c.found.yaml - - fields: - - email - - first_name - - last_name - lists: {} - dispatch_value: null - create: contracts/create_contact/email+first_name+last_name.c3fc7806.create.yaml - found: contracts/create_contact/email+first_name+last_name.c3fc7806.found.yaml - - fields: - - account_city - - first_name - - last_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+first_name+last_name.b6730c1e.create.yaml - found: contracts/create_contact/account_city+first_name+last_name.b6730c1e.found.yaml - - fields: - - account_city - - email - - last_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+last_name.116193ea.create.yaml - found: contracts/create_contact/account_city+email+last_name.116193ea.found.yaml - - fields: - - account_city - - email - - first_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+first_name.0382cd38.create.yaml - found: contracts/create_contact/account_city+email+first_name.0382cd38.found.yaml - - fields: - - account - - first_name - - last_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account+first_name+last_name.646d1dd3.create.yaml - found: contracts/create_contact/account+first_name+last_name.646d1dd3.found.yaml - - fields: - - account - - email - - last_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account+email+last_name.cddfc397.create.yaml - found: contracts/create_contact/account+email+last_name.cddfc397.found.yaml - - fields: - - account - - email - - first_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account+email+first_name.89ddee04.create.yaml - found: contracts/create_contact/account+email+first_name.89ddee04.found.yaml - - fields: - - account - - account_city - - last_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+last_name.31fe521c.create.yaml - found: contracts/create_contact/account+account_city+last_name.31fe521c.found.yaml - - fields: - - account - - account_city - - first_name - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+first_name.94c8b13b.create.yaml - found: contracts/create_contact/account+account_city+first_name.94c8b13b.found.yaml - - fields: - - account - - account_city - - email - lists: {} - dispatch_value: null - create: contracts/create_contact/account+account_city+email.a039b55f.create.yaml - found: contracts/create_contact/account+account_city+email.a039b55f.found.yaml - - fields: - - first_name - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/first_name+last_name+phone+title.759e589f.create.yaml - found: contracts/create_contact/first_name+last_name+phone+title.759e589f.found.yaml - - fields: - - email - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/email+last_name+phone+title.df1cb621.create.yaml - found: contracts/create_contact/email+last_name+phone+title.df1cb621.found.yaml - - fields: - - email - - first_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/email+first_name+phone+title.4eb30bed.create.yaml - found: contracts/create_contact/email+first_name+phone+title.4eb30bed.found.yaml - - fields: - - email - - first_name - - last_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/email+first_name+last_name+title.44a56b67.create.yaml - found: contracts/create_contact/email+first_name+last_name+title.44a56b67.found.yaml - - fields: - - email - - first_name - - last_name - - phone - lists: {} - dispatch_value: null - create: contracts/create_contact/email+first_name+last_name+phone.46fdda46.create.yaml - found: contracts/create_contact/email+first_name+last_name+phone.46fdda46.found.yaml - - fields: - - account_city - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+last_name+phone+title.88c48587.create.yaml - found: contracts/create_contact/account_city+last_name+phone+title.88c48587.found.yaml - - fields: - - account_city - - first_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+first_name+phone+title.79e3e404.create.yaml - found: contracts/create_contact/account_city+first_name+phone+title.79e3e404.found.yaml - - fields: - - account_city - - first_name - - last_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+first_name+last_name+title.869091a0.create.yaml - found: contracts/create_contact/account_city+first_name+last_name+title.869091a0.found.yaml - - fields: - - account_city - - first_name - - last_name - - phone - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+first_name+last_name+phone.e7cd2a47.create.yaml - found: contracts/create_contact/account_city+first_name+last_name+phone.e7cd2a47.found.yaml - - fields: - - account_city - - email - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+phone+title.667b64eb.create.yaml - found: contracts/create_contact/account_city+email+phone+title.667b64eb.found.yaml - - fields: - - account_city - - email - - last_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+last_name+title.72180e70.create.yaml - found: contracts/create_contact/account_city+email+last_name+title.72180e70.found.yaml - - fields: - - account_city - - email - - last_name - - phone - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+last_name+phone.7cf3ab30.create.yaml - found: contracts/create_contact/account_city+email+last_name+phone.7cf3ab30.found.yaml - - fields: - - account_city - - email - - first_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+first_name+title.c501b9af.create.yaml - found: contracts/create_contact/account_city+email+first_name+title.c501b9af.found.yaml - - fields: - - account_city - - email - - first_name - - phone - lists: {} - dispatch_value: null - create: contracts/create_contact/account_city+email+first_name+phone.9350202d.create.yaml - found: contracts/create_contact/account_city+email+first_name+phone.9350202d.found.yaml - - fields: - - account - - last_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+last_name+phone+title.6c3a70e7.create.yaml - found: contracts/create_contact/account+last_name+phone+title.6c3a70e7.found.yaml - - fields: - - account - - first_name - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+first_name+phone+title.441099c8.create.yaml - found: contracts/create_contact/account+first_name+phone+title.441099c8.found.yaml - - fields: - - account - - first_name - - last_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+first_name+last_name+title.d24613f1.create.yaml - found: contracts/create_contact/account+first_name+last_name+title.d24613f1.found.yaml - - fields: - - account - - first_name - - last_name - - phone - lists: {} - dispatch_value: null - create: contracts/create_contact/account+first_name+last_name+phone.83f6dc7e.create.yaml - found: contracts/create_contact/account+first_name+last_name+phone.83f6dc7e.found.yaml - - fields: - - account - - email - - phone - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+email+phone+title.a4ceda1e.create.yaml - found: contracts/create_contact/account+email+phone+title.a4ceda1e.found.yaml - - fields: - - account - - email - - last_name - - title - lists: {} - dispatch_value: null - create: contracts/create_contact/account+email+last_name+title.ac86c876.create.yaml - found: contracts/create_contact/account+email+last_name+title.ac86c876.found.yaml - create_opportunity: - mode: create - main_table: opportunity - parameters: - - account - - account_city - - name - - amount - - currency - - stage - - close_date - identity: [] - lists: [] - dispatch: null - variants: - - fields: - - account - - account_city - - amount - - close_date - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml - found: contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml - - fields: - - account_city - - amount - - close_date - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+amount+close_date+currency+name+stage.3e8d919d.create.yaml - found: contracts/create_opportunity/account_city+amount+close_date+currency+name+stage.3e8d919d.found.yaml - - fields: - - account - - amount - - close_date - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+amount+close_date+currency+name+stage.ee61cdb0.create.yaml - found: contracts/create_opportunity/account+amount+close_date+currency+name+stage.ee61cdb0.found.yaml - - fields: - - account - - account_city - - close_date - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+close_date+currency+name+stage.e6646c16.create.yaml - found: contracts/create_opportunity/account+account_city+close_date+currency+name+stage.e6646c16.found.yaml - - fields: - - account - - account_city - - amount - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+currency+name+stage.1c3e81bb.create.yaml - found: contracts/create_opportunity/account+account_city+amount+currency+name+stage.1c3e81bb.found.yaml - - fields: - - account - - account_city - - amount - - close_date - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+close_date+name+stage.840c2e09.create.yaml - found: contracts/create_opportunity/account+account_city+amount+close_date+name+stage.840c2e09.found.yaml - - fields: - - account - - account_city - - amount - - close_date - - currency - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+close_date+currency+stage.2110b795.create.yaml - found: contracts/create_opportunity/account+account_city+amount+close_date+currency+stage.2110b795.found.yaml - - fields: - - account - - account_city - - amount - - close_date - - currency - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+close_date+currency+name.3d88d046.create.yaml - found: contracts/create_opportunity/account+account_city+amount+close_date+currency+name.3d88d046.found.yaml - - fields: - - amount - - close_date - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/amount+close_date+currency+name+stage.636367d7.create.yaml - found: contracts/create_opportunity/amount+close_date+currency+name+stage.636367d7.found.yaml - - fields: - - account_city - - close_date - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+close_date+currency+name+stage.18017a0a.create.yaml - found: contracts/create_opportunity/account_city+close_date+currency+name+stage.18017a0a.found.yaml - - fields: - - account_city - - amount - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+amount+currency+name+stage.49fd1ee5.create.yaml - found: contracts/create_opportunity/account_city+amount+currency+name+stage.49fd1ee5.found.yaml - - fields: - - account_city - - amount - - close_date - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+amount+close_date+name+stage.74c4d8cd.create.yaml - found: contracts/create_opportunity/account_city+amount+close_date+name+stage.74c4d8cd.found.yaml - - fields: - - account_city - - amount - - close_date - - currency - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+amount+close_date+currency+stage.157c0c72.create.yaml - found: contracts/create_opportunity/account_city+amount+close_date+currency+stage.157c0c72.found.yaml - - fields: - - account_city - - amount - - close_date - - currency - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+amount+close_date+currency+name.6310e515.create.yaml - found: contracts/create_opportunity/account_city+amount+close_date+currency+name.6310e515.found.yaml - - fields: - - account - - close_date - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+close_date+currency+name+stage.b4e0a253.create.yaml - found: contracts/create_opportunity/account+close_date+currency+name+stage.b4e0a253.found.yaml - - fields: - - account - - amount - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+amount+currency+name+stage.b105c423.create.yaml - found: contracts/create_opportunity/account+amount+currency+name+stage.b105c423.found.yaml - - fields: - - account - - amount - - close_date - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+amount+close_date+name+stage.a21562d9.create.yaml - found: contracts/create_opportunity/account+amount+close_date+name+stage.a21562d9.found.yaml - - fields: - - account - - amount - - close_date - - currency - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+amount+close_date+currency+stage.c8c627a9.create.yaml - found: contracts/create_opportunity/account+amount+close_date+currency+stage.c8c627a9.found.yaml - - fields: - - account - - amount - - close_date - - currency - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+amount+close_date+currency+name.a75ae1d8.create.yaml - found: contracts/create_opportunity/account+amount+close_date+currency+name.a75ae1d8.found.yaml - - fields: - - account - - account_city - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+currency+name+stage.221bd3cc.create.yaml - found: contracts/create_opportunity/account+account_city+currency+name+stage.221bd3cc.found.yaml - - fields: - - account - - account_city - - close_date - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+close_date+name+stage.bce513f8.create.yaml - found: contracts/create_opportunity/account+account_city+close_date+name+stage.bce513f8.found.yaml - - fields: - - account - - account_city - - close_date - - currency - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+close_date+currency+stage.9f763456.create.yaml - found: contracts/create_opportunity/account+account_city+close_date+currency+stage.9f763456.found.yaml - - fields: - - account - - account_city - - close_date - - currency - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+close_date+currency+name.53170b23.create.yaml - found: contracts/create_opportunity/account+account_city+close_date+currency+name.53170b23.found.yaml - - fields: - - account - - account_city - - amount - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+name+stage.940c25af.create.yaml - found: contracts/create_opportunity/account+account_city+amount+name+stage.940c25af.found.yaml - - fields: - - account - - account_city - - amount - - currency - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+currency+stage.f84f8946.create.yaml - found: contracts/create_opportunity/account+account_city+amount+currency+stage.f84f8946.found.yaml - - fields: - - account - - account_city - - amount - - currency - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+currency+name.9d5c7892.create.yaml - found: contracts/create_opportunity/account+account_city+amount+currency+name.9d5c7892.found.yaml - - fields: - - account - - account_city - - amount - - close_date - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+close_date+stage.4fdcd48d.create.yaml - found: contracts/create_opportunity/account+account_city+amount+close_date+stage.4fdcd48d.found.yaml - - fields: - - account - - account_city - - amount - - close_date - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+close_date+name.c265ff6a.create.yaml - found: contracts/create_opportunity/account+account_city+amount+close_date+name.c265ff6a.found.yaml - - fields: - - account - - account_city - - amount - - close_date - - currency - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+close_date+currency.19ea66ae.create.yaml - found: contracts/create_opportunity/account+account_city+amount+close_date+currency.19ea66ae.found.yaml - - fields: - - close_date - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/close_date+currency+name+stage.6196f741.create.yaml - found: contracts/create_opportunity/close_date+currency+name+stage.6196f741.found.yaml - - fields: - - amount - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/amount+currency+name+stage.bd43a82e.create.yaml - found: contracts/create_opportunity/amount+currency+name+stage.bd43a82e.found.yaml - - fields: - - amount - - close_date - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/amount+close_date+name+stage.2a87af45.create.yaml - found: contracts/create_opportunity/amount+close_date+name+stage.2a87af45.found.yaml - - fields: - - amount - - close_date - - currency - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/amount+close_date+currency+stage.d62f80cd.create.yaml - found: contracts/create_opportunity/amount+close_date+currency+stage.d62f80cd.found.yaml - - fields: - - amount - - close_date - - currency - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/amount+close_date+currency+name.6c521040.create.yaml - found: contracts/create_opportunity/amount+close_date+currency+name.6c521040.found.yaml - - fields: - - account_city - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+currency+name+stage.b7663184.create.yaml - found: contracts/create_opportunity/account_city+currency+name+stage.b7663184.found.yaml - - fields: - - account_city - - close_date - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+close_date+name+stage.d3bed3d5.create.yaml - found: contracts/create_opportunity/account_city+close_date+name+stage.d3bed3d5.found.yaml - - fields: - - account_city - - close_date - - currency - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+close_date+currency+stage.cc489929.create.yaml - found: contracts/create_opportunity/account_city+close_date+currency+stage.cc489929.found.yaml - - fields: - - account_city - - close_date - - currency - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+close_date+currency+name.0ec4ae28.create.yaml - found: contracts/create_opportunity/account_city+close_date+currency+name.0ec4ae28.found.yaml - - fields: - - account_city - - amount - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+amount+name+stage.44542cc2.create.yaml - found: contracts/create_opportunity/account_city+amount+name+stage.44542cc2.found.yaml - - fields: - - account_city - - amount - - currency - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+amount+currency+stage.01773cbe.create.yaml - found: contracts/create_opportunity/account_city+amount+currency+stage.01773cbe.found.yaml - - fields: - - account_city - - amount - - currency - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+amount+currency+name.964d6501.create.yaml - found: contracts/create_opportunity/account_city+amount+currency+name.964d6501.found.yaml - - fields: - - account_city - - amount - - close_date - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+amount+close_date+stage.05eb4131.create.yaml - found: contracts/create_opportunity/account_city+amount+close_date+stage.05eb4131.found.yaml - - fields: - - account_city - - amount - - close_date - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+amount+close_date+name.691edf71.create.yaml - found: contracts/create_opportunity/account_city+amount+close_date+name.691edf71.found.yaml - - fields: - - account_city - - amount - - close_date - - currency - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account_city+amount+close_date+currency.5f3eb90a.create.yaml - found: contracts/create_opportunity/account_city+amount+close_date+currency.5f3eb90a.found.yaml - - fields: - - account - - currency - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+currency+name+stage.ef841eea.create.yaml - found: contracts/create_opportunity/account+currency+name+stage.ef841eea.found.yaml - - fields: - - account - - close_date - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+close_date+name+stage.9dec0d83.create.yaml - found: contracts/create_opportunity/account+close_date+name+stage.9dec0d83.found.yaml - - fields: - - account - - close_date - - currency - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+close_date+currency+stage.67034ba2.create.yaml - found: contracts/create_opportunity/account+close_date+currency+stage.67034ba2.found.yaml - - fields: - - account - - close_date - - currency - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+close_date+currency+name.430d7e5a.create.yaml - found: contracts/create_opportunity/account+close_date+currency+name.430d7e5a.found.yaml - - fields: - - account - - amount - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+amount+name+stage.cc3f78e3.create.yaml - found: contracts/create_opportunity/account+amount+name+stage.cc3f78e3.found.yaml - - fields: - - account - - amount - - currency - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+amount+currency+stage.d361f29b.create.yaml - found: contracts/create_opportunity/account+amount+currency+stage.d361f29b.found.yaml - - fields: - - account - - amount - - currency - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+amount+currency+name.9346646b.create.yaml - found: contracts/create_opportunity/account+amount+currency+name.9346646b.found.yaml - - fields: - - account - - amount - - close_date - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+amount+close_date+stage.b840114e.create.yaml - found: contracts/create_opportunity/account+amount+close_date+stage.b840114e.found.yaml - - fields: - - account - - amount - - close_date - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+amount+close_date+name.510970cc.create.yaml - found: contracts/create_opportunity/account+amount+close_date+name.510970cc.found.yaml - - fields: - - account - - amount - - close_date - - currency - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+amount+close_date+currency.add6b978.create.yaml - found: contracts/create_opportunity/account+amount+close_date+currency.add6b978.found.yaml - - fields: - - account - - account_city - - name - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+name+stage.79f9642c.create.yaml - found: contracts/create_opportunity/account+account_city+name+stage.79f9642c.found.yaml - - fields: - - account - - account_city - - currency - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+currency+stage.ab0ce4e4.create.yaml - found: contracts/create_opportunity/account+account_city+currency+stage.ab0ce4e4.found.yaml - - fields: - - account - - account_city - - currency - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+currency+name.37d28d7b.create.yaml - found: contracts/create_opportunity/account+account_city+currency+name.37d28d7b.found.yaml - - fields: - - account - - account_city - - close_date - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+close_date+stage.e2243a69.create.yaml - found: contracts/create_opportunity/account+account_city+close_date+stage.e2243a69.found.yaml - - fields: - - account - - account_city - - close_date - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+close_date+name.7eba75e7.create.yaml - found: contracts/create_opportunity/account+account_city+close_date+name.7eba75e7.found.yaml - - fields: - - account - - account_city - - close_date - - currency - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+close_date+currency.f4da1bc2.create.yaml - found: contracts/create_opportunity/account+account_city+close_date+currency.f4da1bc2.found.yaml - - fields: - - account - - account_city - - amount - - stage - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+stage.ef3af8a2.create.yaml - found: contracts/create_opportunity/account+account_city+amount+stage.ef3af8a2.found.yaml - - fields: - - account - - account_city - - amount - - name - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+name.898ff6d9.create.yaml - found: contracts/create_opportunity/account+account_city+amount+name.898ff6d9.found.yaml - - fields: - - account - - account_city - - amount - - currency - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+currency.54e9d8de.create.yaml - found: contracts/create_opportunity/account+account_city+amount+currency.54e9d8de.found.yaml - - fields: - - account - - account_city - - amount - - close_date - lists: {} - dispatch_value: null - create: contracts/create_opportunity/account+account_city+amount+close_date.08f5c040.create.yaml - found: contracts/create_opportunity/account+account_city+amount+close_date.08f5c040.found.yaml - update_lead: - mode: update - main_table: lead - parameters: - - first_name - - last_name - - company - - field - - value - identity: - - company - - first_name - - last_name - lists: [] - dispatch: field - variants: - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: accountName - update: contracts/update_lead/accountName+company+field+first_name+last_name+value.f0188cc9.update.yaml - found: contracts/update_lead/accountName+company+field+first_name+last_name+value.f0188cc9.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: account_name - update: contracts/update_lead/account_name+company+field+first_name+last_name+value.112a6b3c.update.yaml - found: contracts/update_lead/account_name+company+field+first_name+last_name+value.112a6b3c.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: addressCity - update: contracts/update_lead/addressCity+company+field+first_name+last_name+value.2e349c1f.update.yaml - found: contracts/update_lead/addressCity+company+field+first_name+last_name+value.2e349c1f.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: addressCountry - update: contracts/update_lead/addressCountry+company+field+first_name+last_name+value.cf5a4b98.update.yaml - found: contracts/update_lead/addressCountry+company+field+first_name+last_name+value.cf5a4b98.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: addressPostalCode - update: contracts/update_lead/addressPostalCode+company+field+first_name+last_name+value.1b559d01.update.yaml - found: contracts/update_lead/addressPostalCode+company+field+first_name+last_name+value.1b559d01.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: addressState - update: contracts/update_lead/addressState+company+field+first_name+last_name+value.d8fcca02.update.yaml - found: contracts/update_lead/addressState+company+field+first_name+last_name+value.d8fcca02.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: addressStreet - update: contracts/update_lead/addressStreet+company+field+first_name+last_name+value.ccdba0fe.update.yaml - found: contracts/update_lead/addressStreet+company+field+first_name+last_name+value.ccdba0fe.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: address_city - update: contracts/update_lead/address_city+company+field+first_name+last_name+value.39516a5c.update.yaml - found: contracts/update_lead/address_city+company+field+first_name+last_name+value.39516a5c.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: address_country - update: contracts/update_lead/address_country+company+field+first_name+last_name+value.26370712.update.yaml - found: contracts/update_lead/address_country+company+field+first_name+last_name+value.26370712.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: address_postal_code - update: contracts/update_lead/address_postal_code+company+field+first_name+last_name+value.b15ffc5b.update.yaml - found: contracts/update_lead/address_postal_code+company+field+first_name+last_name+value.b15ffc5b.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: address_state - update: contracts/update_lead/address_state+company+field+first_name+last_name+value.787f1586.update.yaml - found: contracts/update_lead/address_state+company+field+first_name+last_name+value.787f1586.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: address_street - update: contracts/update_lead/address_street+company+field+first_name+last_name+value.abad9297.update.yaml - found: contracts/update_lead/address_street+company+field+first_name+last_name+value.abad9297.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: assignedUserId - update: contracts/update_lead/assignedUserId+company+field+first_name+last_name+value.d92ef246.update.yaml - found: contracts/update_lead/assignedUserId+company+field+first_name+last_name+value.d92ef246.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: assigned_user_id - update: contracts/update_lead/assigned_user_id+company+field+first_name+last_name+value.e65072f2.update.yaml - found: contracts/update_lead/assigned_user_id+company+field+first_name+last_name+value.e65072f2.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: campaignId - update: contracts/update_lead/campaignId+company+field+first_name+last_name+value.5d7c3f99.update.yaml - found: contracts/update_lead/campaignId+company+field+first_name+last_name+value.5d7c3f99.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: campaign_id - update: contracts/update_lead/campaign_id+company+field+first_name+last_name+value.2ba0bedb.update.yaml - found: contracts/update_lead/campaign_id+company+field+first_name+last_name+value.2ba0bedb.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: createdAccountId - update: contracts/update_lead/createdAccountId+company+field+first_name+last_name+value.1e11a836.update.yaml - found: contracts/update_lead/createdAccountId+company+field+first_name+last_name+value.1e11a836.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: createdById - update: contracts/update_lead/createdById+company+field+first_name+last_name+value.ce7b1b24.update.yaml - found: contracts/update_lead/createdById+company+field+first_name+last_name+value.ce7b1b24.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: createdContactId - update: contracts/update_lead/createdContactId+company+field+first_name+last_name+value.5e8f2aec.update.yaml - found: contracts/update_lead/createdContactId+company+field+first_name+last_name+value.5e8f2aec.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: createdOpportunityId - update: contracts/update_lead/createdOpportunityId+company+field+first_name+last_name+value.57ce21be.update.yaml - found: contracts/update_lead/createdOpportunityId+company+field+first_name+last_name+value.57ce21be.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: created_account_id - update: contracts/update_lead/created_account_id+company+field+first_name+last_name+value.61ccbd38.update.yaml - found: contracts/update_lead/created_account_id+company+field+first_name+last_name+value.61ccbd38.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: created_by_id - update: contracts/update_lead/created_by_id+company+field+first_name+last_name+value.afd73f3f.update.yaml - found: contracts/update_lead/created_by_id+company+field+first_name+last_name+value.afd73f3f.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: created_contact_id - update: contracts/update_lead/created_contact_id+company+field+first_name+last_name+value.46f293a5.update.yaml - found: contracts/update_lead/created_contact_id+company+field+first_name+last_name+value.46f293a5.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: created_opportunity_id - update: contracts/update_lead/created_opportunity_id+company+field+first_name+last_name+value.45f20089.update.yaml - found: contracts/update_lead/created_opportunity_id+company+field+first_name+last_name+value.45f20089.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: description - update: contracts/update_lead/description+company+field+first_name+last_name+value.f7f378eb.update.yaml - found: contracts/update_lead/description+company+field+first_name+last_name+value.f7f378eb.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: email - update: contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml - found: contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: firstName - update: contracts/update_lead/firstName+company+field+first_name+last_name+value.df020818.update.yaml - found: contracts/update_lead/firstName+company+field+first_name+last_name+value.df020818.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: first_name - update: contracts/update_lead/first_name+company+field+first_name+last_name+value.fe20eac3.update.yaml - found: contracts/update_lead/first_name+company+field+first_name+last_name+value.fe20eac3.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: industry - update: contracts/update_lead/industry+company+field+first_name+last_name+value.681c4e87.update.yaml - found: contracts/update_lead/industry+company+field+first_name+last_name+value.681c4e87.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: lastName - update: contracts/update_lead/lastName+company+field+first_name+last_name+value.20ff2853.update.yaml - found: contracts/update_lead/lastName+company+field+first_name+last_name+value.20ff2853.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: last_name - update: contracts/update_lead/last_name+company+field+first_name+last_name+value.aa922c91.update.yaml - found: contracts/update_lead/last_name+company+field+first_name+last_name+value.aa922c91.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: middleName - update: contracts/update_lead/middleName+company+field+first_name+last_name+value.e12f42a6.update.yaml - found: contracts/update_lead/middleName+company+field+first_name+last_name+value.e12f42a6.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: middle_name - update: contracts/update_lead/middle_name+company+field+first_name+last_name+value.95efddfe.update.yaml - found: contracts/update_lead/middle_name+company+field+first_name+last_name+value.95efddfe.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: modifiedById - update: contracts/update_lead/modifiedById+company+field+first_name+last_name+value.1cbe39b1.update.yaml - found: contracts/update_lead/modifiedById+company+field+first_name+last_name+value.1cbe39b1.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: modified_by_id - update: contracts/update_lead/modified_by_id+company+field+first_name+last_name+value.f3a3d0bb.update.yaml - found: contracts/update_lead/modified_by_id+company+field+first_name+last_name+value.f3a3d0bb.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: opportunityAmountCurrency - update: contracts/update_lead/opportunityAmountCurrency+company+field+first_name+last_name+value.fe84c11f.update.yaml - found: contracts/update_lead/opportunityAmountCurrency+company+field+first_name+last_name+value.fe84c11f.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: opportunity_amount_currency - update: contracts/update_lead/opportunity_amount_currency+company+field+first_name+last_name+value.b0a33e68.update.yaml - found: contracts/update_lead/opportunity_amount_currency+company+field+first_name+last_name+value.b0a33e68.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: salutationName - update: contracts/update_lead/salutationName+company+field+first_name+last_name+value.f6ad2278.update.yaml - found: contracts/update_lead/salutationName+company+field+first_name+last_name+value.f6ad2278.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: salutation_name - update: contracts/update_lead/salutation_name+company+field+first_name+last_name+value.f8404b60.update.yaml - found: contracts/update_lead/salutation_name+company+field+first_name+last_name+value.f8404b60.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: source - update: contracts/update_lead/source+company+field+first_name+last_name+value.e5ca3051.update.yaml - found: contracts/update_lead/source+company+field+first_name+last_name+value.e5ca3051.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: status - update: contracts/update_lead/status+company+field+first_name+last_name+value.d77469a2.update.yaml - found: contracts/update_lead/status+company+field+first_name+last_name+value.d77469a2.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: title - update: contracts/update_lead/title+company+field+first_name+last_name+value.39d15090.update.yaml - found: contracts/update_lead/title+company+field+first_name+last_name+value.39d15090.found.yaml - - fields: - - company - - field - - first_name - - last_name - - value - lists: {} - dispatch_value: website - update: contracts/update_lead/website+company+field+first_name+last_name+value.fdf2495b.update.yaml - found: contracts/update_lead/website+company+field+first_name+last_name+value.fdf2495b.found.yaml - log_call: - mode: create - main_table: call - parameters: - - subject - - status - - direction - - date_start - - duration_minutes - - account - - account_city - - contact - - lead - - lead_company - - description - identity: [] - lists: [] - dispatch: null - variants: - - fields: - - account - - account_city - - contact - - date_start - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml - found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml - - fields: - - date_start - - direction - - duration_minutes - - lead - - lead_company - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status+subject.1c40818d.create.yaml - found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status+subject.1c40818d.found.yaml - - fields: - - account_city - - contact - - date_start - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status+su.a1e6dea7.create.yaml - found: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status+su.a1e6dea7.found.yaml - - fields: - - account - - contact - - date_start - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status+subject.ac259c2d.create.yaml - found: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status+subject.ac259c2d.found.yaml - - fields: - - account - - account_city - - date_start - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status+su.40bb5ef5.create.yaml - found: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status+su.40bb5ef5.found.yaml - - fields: - - account - - account_city - - contact - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status+subje.13cace1e.create.yaml - found: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status+subje.13cace1e.found.yaml - - fields: - - account - - account_city - - contact - - date_start - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status+subjec.6fa3c615.create.yaml - found: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status+subjec.6fa3c615.found.yaml - - fields: - - account - - account_city - - contact - - date_start - - description - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status+subj.375289e9.create.yaml - found: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status+subj.375289e9.found.yaml - - fields: - - account - - account_city - - contact - - date_start - - description - - direction - - duration_minutes - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.f9fb0e0d.create.yaml - found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.f9fb0e0d.found.yaml - - fields: - - account - - account_city - - contact - - date_start - - description - - direction - - duration_minutes - - status - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.fa682ac0.create.yaml - found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.fa682ac0.found.yaml - - fields: - - direction - - duration_minutes - - lead - - lead_company - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/direction+duration_minutes+lead+lead_company+status+subject.582a1975.create.yaml - found: contracts/log_call/direction+duration_minutes+lead+lead_company+status+subject.582a1975.found.yaml - - fields: - - date_start - - duration_minutes - - lead - - lead_company - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+duration_minutes+lead+lead_company+status+subject.cd43c2c6.create.yaml - found: contracts/log_call/date_start+duration_minutes+lead+lead_company+status+subject.cd43c2c6.found.yaml - - fields: - - date_start - - direction - - duration_minutes - - lead_company - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+direction+duration_minutes+lead_company+status+subject.d1a9d93e.create.yaml - found: contracts/log_call/date_start+direction+duration_minutes+lead_company+status+subject.d1a9d93e.found.yaml - - fields: - - date_start - - direction - - duration_minutes - - lead - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+direction+duration_minutes+lead+status+subject.e3f86e48.create.yaml - found: contracts/log_call/date_start+direction+duration_minutes+lead+status+subject.e3f86e48.found.yaml - - fields: - - date_start - - direction - - duration_minutes - - lead - - lead_company - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+subject.7cd2c97e.create.yaml - found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+subject.7cd2c97e.found.yaml - - fields: - - date_start - - direction - - duration_minutes - - lead - - lead_company - - status - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status.fe5bab1b.create.yaml - found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company+status.fe5bab1b.found.yaml - - fields: - - contact - - date_start - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/contact+date_start+description+direction+duration_minutes+status+subject.ecb41244.create.yaml - found: contracts/log_call/contact+date_start+description+direction+duration_minutes+status+subject.ecb41244.found.yaml - - fields: - - account_city - - date_start - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account_city+date_start+description+direction+duration_minutes+status+subject.72b09e7c.create.yaml - found: contracts/log_call/account_city+date_start+description+direction+duration_minutes+status+subject.72b09e7c.found.yaml - - fields: - - account_city - - contact - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account_city+contact+description+direction+duration_minutes+status+subject.6027050a.create.yaml - found: contracts/log_call/account_city+contact+description+direction+duration_minutes+status+subject.6027050a.found.yaml - - fields: - - account_city - - contact - - date_start - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account_city+contact+date_start+direction+duration_minutes+status+subject.fe2cf091.create.yaml - found: contracts/log_call/account_city+contact+date_start+direction+duration_minutes+status+subject.fe2cf091.found.yaml - - fields: - - account_city - - contact - - date_start - - description - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account_city+contact+date_start+description+duration_minutes+status+subject.933068cc.create.yaml - found: contracts/log_call/account_city+contact+date_start+description+duration_minutes+status+subject.933068cc.found.yaml - - fields: - - account_city - - contact - - date_start - - description - - direction - - duration_minutes - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+subject.53ca7468.create.yaml - found: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+subject.53ca7468.found.yaml - - fields: - - account_city - - contact - - date_start - - description - - direction - - duration_minutes - - status - lists: {} - dispatch_value: null - create: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status.502d3047.create.yaml - found: contracts/log_call/account_city+contact+date_start+description+direction+duration_minutes+status.502d3047.found.yaml - - fields: - - account - - date_start - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+date_start+description+direction+duration_minutes+status+subject.461a3bbb.create.yaml - found: contracts/log_call/account+date_start+description+direction+duration_minutes+status+subject.461a3bbb.found.yaml - - fields: - - account - - contact - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+contact+description+direction+duration_minutes+status+subject.4eeb71c8.create.yaml - found: contracts/log_call/account+contact+description+direction+duration_minutes+status+subject.4eeb71c8.found.yaml - - fields: - - account - - contact - - date_start - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+contact+date_start+direction+duration_minutes+status+subject.88c05db0.create.yaml - found: contracts/log_call/account+contact+date_start+direction+duration_minutes+status+subject.88c05db0.found.yaml - - fields: - - account - - contact - - date_start - - description - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+contact+date_start+description+duration_minutes+status+subject.92c40c7a.create.yaml - found: contracts/log_call/account+contact+date_start+description+duration_minutes+status+subject.92c40c7a.found.yaml - - fields: - - account - - contact - - date_start - - description - - direction - - duration_minutes - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+subject.36ca60f2.create.yaml - found: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+subject.36ca60f2.found.yaml - - fields: - - account - - contact - - date_start - - description - - direction - - duration_minutes - - status - lists: {} - dispatch_value: null - create: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status.f18701ec.create.yaml - found: contracts/log_call/account+contact+date_start+description+direction+duration_minutes+status.f18701ec.found.yaml - - fields: - - account - - account_city - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+description+direction+duration_minutes+status+subject.2e513d2e.create.yaml - found: contracts/log_call/account+account_city+description+direction+duration_minutes+status+subject.2e513d2e.found.yaml - - fields: - - account - - account_city - - date_start - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+date_start+direction+duration_minutes+status+subject.5dd1577d.create.yaml - found: contracts/log_call/account+account_city+date_start+direction+duration_minutes+status+subject.5dd1577d.found.yaml - - fields: - - account - - account_city - - date_start - - description - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+date_start+description+duration_minutes+status+subject.c2f5c493.create.yaml - found: contracts/log_call/account+account_city+date_start+description+duration_minutes+status+subject.c2f5c493.found.yaml - - fields: - - account - - account_city - - date_start - - description - - direction - - duration_minutes - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+subject.1444168f.create.yaml - found: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+subject.1444168f.found.yaml - - fields: - - account - - account_city - - date_start - - description - - direction - - duration_minutes - - status - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status.6b8a69ef.create.yaml - found: contracts/log_call/account+account_city+date_start+description+direction+duration_minutes+status.6b8a69ef.found.yaml - - fields: - - account - - account_city - - contact - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+direction+duration_minutes+status+subject.25d5283f.create.yaml - found: contracts/log_call/account+account_city+contact+direction+duration_minutes+status+subject.25d5283f.found.yaml - - fields: - - account - - account_city - - contact - - description - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+description+duration_minutes+status+subject.994dc4c5.create.yaml - found: contracts/log_call/account+account_city+contact+description+duration_minutes+status+subject.994dc4c5.found.yaml - - fields: - - account - - account_city - - contact - - description - - direction - - duration_minutes - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+subject.5213a5ad.create.yaml - found: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+subject.5213a5ad.found.yaml - - fields: - - account - - account_city - - contact - - description - - direction - - duration_minutes - - status - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status.a5f10536.create.yaml - found: contracts/log_call/account+account_city+contact+description+direction+duration_minutes+status.a5f10536.found.yaml - - fields: - - account - - account_city - - contact - - date_start - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+date_start+duration_minutes+status+subject.a72c4dca.create.yaml - found: contracts/log_call/account+account_city+contact+date_start+duration_minutes+status+subject.a72c4dca.found.yaml - - fields: - - account - - account_city - - contact - - date_start - - direction - - duration_minutes - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+subject.a261cf3e.create.yaml - found: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+subject.a261cf3e.found.yaml - - fields: - - account - - account_city - - contact - - date_start - - direction - - duration_minutes - - status - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status.97f875f0.create.yaml - found: contracts/log_call/account+account_city+contact+date_start+direction+duration_minutes+status.97f875f0.found.yaml - - fields: - - account - - account_city - - contact - - date_start - - description - - duration_minutes - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+subject.2740ed8a.create.yaml - found: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+subject.2740ed8a.found.yaml - - fields: - - account - - account_city - - contact - - date_start - - description - - duration_minutes - - status - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status.eec71d6e.create.yaml - found: contracts/log_call/account+account_city+contact+date_start+description+duration_minutes+status.eec71d6e.found.yaml - - fields: - - account - - account_city - - contact - - date_start - - description - - direction - - duration_minutes - lists: {} - dispatch_value: null - create: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes.d2b14413.create.yaml - found: contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes.d2b14413.found.yaml - - fields: - - duration_minutes - - lead - - lead_company - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/duration_minutes+lead+lead_company+status+subject.248320eb.create.yaml - found: contracts/log_call/duration_minutes+lead+lead_company+status+subject.248320eb.found.yaml - - fields: - - direction - - duration_minutes - - lead_company - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/direction+duration_minutes+lead_company+status+subject.31e67761.create.yaml - found: contracts/log_call/direction+duration_minutes+lead_company+status+subject.31e67761.found.yaml - - fields: - - direction - - duration_minutes - - lead - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/direction+duration_minutes+lead+status+subject.80c440d6.create.yaml - found: contracts/log_call/direction+duration_minutes+lead+status+subject.80c440d6.found.yaml - - fields: - - direction - - duration_minutes - - lead - - lead_company - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/direction+duration_minutes+lead+lead_company+subject.de5e153e.create.yaml - found: contracts/log_call/direction+duration_minutes+lead+lead_company+subject.de5e153e.found.yaml - - fields: - - direction - - duration_minutes - - lead - - lead_company - - status - lists: {} - dispatch_value: null - create: contracts/log_call/direction+duration_minutes+lead+lead_company+status.0ab0a0ff.create.yaml - found: contracts/log_call/direction+duration_minutes+lead+lead_company+status.0ab0a0ff.found.yaml - - fields: - - date_start - - duration_minutes - - lead_company - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+duration_minutes+lead_company+status+subject.88eff1f9.create.yaml - found: contracts/log_call/date_start+duration_minutes+lead_company+status+subject.88eff1f9.found.yaml - - fields: - - date_start - - duration_minutes - - lead - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+duration_minutes+lead+status+subject.1bf286d5.create.yaml - found: contracts/log_call/date_start+duration_minutes+lead+status+subject.1bf286d5.found.yaml - - fields: - - date_start - - duration_minutes - - lead - - lead_company - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+duration_minutes+lead+lead_company+subject.f23cce08.create.yaml - found: contracts/log_call/date_start+duration_minutes+lead+lead_company+subject.f23cce08.found.yaml - - fields: - - date_start - - duration_minutes - - lead - - lead_company - - status - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+duration_minutes+lead+lead_company+status.5a81f788.create.yaml - found: contracts/log_call/date_start+duration_minutes+lead+lead_company+status.5a81f788.found.yaml - - fields: - - date_start - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+direction+duration_minutes+status+subject.023b89f7.create.yaml - found: contracts/log_call/date_start+direction+duration_minutes+status+subject.023b89f7.found.yaml - - fields: - - date_start - - direction - - duration_minutes - - lead_company - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+direction+duration_minutes+lead_company+subject.8cb30c88.create.yaml - found: contracts/log_call/date_start+direction+duration_minutes+lead_company+subject.8cb30c88.found.yaml - - fields: - - date_start - - direction - - duration_minutes - - lead_company - - status - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+direction+duration_minutes+lead_company+status.93dd5488.create.yaml - found: contracts/log_call/date_start+direction+duration_minutes+lead_company+status.93dd5488.found.yaml - - fields: - - date_start - - direction - - duration_minutes - - lead - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+direction+duration_minutes+lead+subject.8d8612d3.create.yaml - found: contracts/log_call/date_start+direction+duration_minutes+lead+subject.8d8612d3.found.yaml - - fields: - - date_start - - direction - - duration_minutes - - lead - - status - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+direction+duration_minutes+lead+status.500ced17.create.yaml - found: contracts/log_call/date_start+direction+duration_minutes+lead+status.500ced17.found.yaml - - fields: - - date_start - - direction - - duration_minutes - - lead - - lead_company - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company.9c7e8372.create.yaml - found: contracts/log_call/date_start+direction+duration_minutes+lead+lead_company.9c7e8372.found.yaml - - fields: - - date_start - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/date_start+description+direction+duration_minutes+status+subject.2e2c7e7a.create.yaml - found: contracts/log_call/date_start+description+direction+duration_minutes+status+subject.2e2c7e7a.found.yaml - - fields: - - contact - - description - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/contact+description+direction+duration_minutes+status+subject.47ddf296.create.yaml - found: contracts/log_call/contact+description+direction+duration_minutes+status+subject.47ddf296.found.yaml - - fields: - - contact - - date_start - - direction - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/contact+date_start+direction+duration_minutes+status+subject.948cf7ee.create.yaml - found: contracts/log_call/contact+date_start+direction+duration_minutes+status+subject.948cf7ee.found.yaml - - fields: - - contact - - date_start - - description - - duration_minutes - - status - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/contact+date_start+description+duration_minutes+status+subject.9f7503e2.create.yaml - found: contracts/log_call/contact+date_start+description+duration_minutes+status+subject.9f7503e2.found.yaml - - fields: - - contact - - date_start - - description - - direction - - duration_minutes - - subject - lists: {} - dispatch_value: null - create: contracts/log_call/contact+date_start+description+direction+duration_minutes+subject.9f27d6b5.create.yaml - found: contracts/log_call/contact+date_start+description+direction+duration_minutes+subject.9f27d6b5.found.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/espocrm/report.json b/packages/flow/benchmark/generate_contracts/results/espocrm/report.json deleted file mode 100644 index 880ea3e522..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/espocrm/report.json +++ /dev/null @@ -1,278 +0,0 @@ -{ - "applied": [], - "read_account": [ - "the read account holds TRIGGER (needed to see triggers; it can also create and drop triggers, so keep its password from the agent)" - ], - "dialect": "mysql", - "tables": 141, - "questions": [ - { - "number": 1, - "key": "phones", - "text": "These values were stored as other text with the same digits (a phone number written with spaces stored without): create_contact.phone. Compare them as phone numbers (the same digits, at least 7, and a task value that isn't shaped like a phone number is compared exactly)? Answer `phones: [, ...]` with the ones that are phone numbers; any other is compared exactly.", - "proposal": [ - "create_contact.phone" - ], - "answered": true - }, - { - "number": 2, - "key": "bookkeeping", - "text": "Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (account: the account row the task's account, account_city name; lead: the lead row the task's lead_company name, the target row): account: stream_updated_at; lead: modified_at, modified_by_id, stream_updated_at. Changed on rows the generator can't identify, so a write that changes them stops: contact: stream_updated_at. Rows the app adds beside each write, expected exactly that many times: action_history_record (1 per write), auth_log_record (1 per write), call_user (1 per write), note (1 per write). Tables that gained a different number of rows in different demonstrations; they are not logs unless you list them under logs_add (rows may then be added, existing rows must not change): note, note_user. Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): account_contact: role; call: created_at, modified_at, modified_by_id, parent_type, uid; contact: address_city, address_country, address_postal_code, address_state, address_street, assigned_user_id, campaign_id, created_at, description, middle_name, modified_at, modified_by_id, salutation_name, stream_updated_at; opportunity: assigned_user_id, campaign_id, contact_id, created_at, description, last_stage, lead_source, modified_at, modified_by_id, probability, stream_updated_at. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log.", - "proposal": "accept", - "answered": true - }, - { - "number": 3, - "key": "recheck_after_s", - "text": "How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `.", - "proposal": 300, - "answered": true - } - ], - "kinds": { - "create_contact": { - "mode": "create", - "main_table": "contact", - "locations": { - "account": "contact.account_id -> account.name (exact)", - "account_city": "contact.account_id -> account.billing_address_city (exact)", - "email": "entity_email_address (whose entity_id is the contact key).email_address_id -> email_address.name (email)", - "first_name": "contact.first_name (exact)", - "last_name": "contact.last_name (exact)", - "phone": "entity_phone_number (whose entity_id is the contact key).phone_number_id -> phone_number.name (phone)", - "title": "account_contact (whose contact_id is the contact key).role (exact)" - }, - "placed_by_name": [], - "versions": null, - "placed_from_another_kind": [], - "demonstrations": 2, - "children": { - "account#2": "account_contact", - "account_city#2": "account_contact", - "email": "entity_email_address", - "phone": "entity_phone_number", - "title": "account_contact" - }, - "dispatch_children": {}, - "lists": {}, - "side_rows": [ - { - "table": "note", - "count": 1, - "unless": null - } - ], - "logs": [ - "action_history_record", - "auth_log_record" - ], - "log_candidates": [], - "touched_rows": [ - { - "table": "account", - "selector": { - "name": { - "param": "account" - }, - "billing_address_city": { - "param": "account_city" - } - }, - "columns": { - "stream_updated_at": "timestamp" - }, - "row": "the account row the task's account, account_city name", - "by_key": false - } - ], - "unexcused": {}, - "refused": [], - "unresolved": {}, - "emitted": 64, - "rejected": [] - }, - "create_opportunity": { - "mode": "create", - "main_table": "opportunity", - "locations": { - "account": "opportunity.account_id -> account.name (exact)", - "account_city": "opportunity.account_id -> account.billing_address_city (exact)", - "amount": "opportunity.amount (number)", - "close_date": "opportunity.close_date (date)", - "currency": "opportunity.amount_currency (exact)", - "name": "opportunity.name (exact)", - "stage": "opportunity.stage (exact)" - }, - "placed_by_name": [], - "versions": null, - "placed_from_another_kind": [], - "demonstrations": 2, - "children": {}, - "dispatch_children": {}, - "lists": {}, - "side_rows": [ - { - "table": "note", - "count": 1, - "unless": null - } - ], - "logs": [ - "action_history_record", - "auth_log_record" - ], - "log_candidates": [], - "touched_rows": [ - { - "table": "account", - "selector": { - "name": { - "param": "account" - }, - "billing_address_city": { - "param": "account_city" - } - }, - "columns": { - "stream_updated_at": "timestamp" - }, - "row": "the account row the task's account, account_city name", - "by_key": false - } - ], - "unexcused": {}, - "refused": [], - "unresolved": {}, - "emitted": 64, - "rejected": [] - }, - "update_lead": { - "mode": "update", - "main_table": "lead", - "locations": { - "company": "lead.account_name (exact)", - "first_name": "lead.first_name (exact)", - "last_name": "lead.last_name (exact)" - }, - "placed_by_name": [], - "versions": { - "kind": "audit", - "table": "action_history_record", - "record_column": "target_id", - "author_column": "user_id", - "actor_param": "bot_user_id", - "where": { - "target_type": "Lead", - "action": "update" - } - }, - "placed_from_another_kind": [], - "demonstrations": 2, - "children": {}, - "dispatch_children": { - "email": { - "value@email": "entity_email_address" - } - }, - "lists": {}, - "side_rows": [], - "logs": [ - "action_history_record", - "auth_log_record" - ], - "log_candidates": [ - "note" - ], - "touched_rows": [], - "unexcused": {}, - "refused": [], - "unresolved": {}, - "emitted": 43, - "rejected": [] - }, - "log_call": { - "mode": "create", - "main_table": "call", - "locations": { - "account": "call.account_id -> account.name (exact)", - "account_city": "call.account_id -> account.billing_address_city (exact)", - "contact": "call_contact (whose call_id is the call key).contact_id -> contact.first_name || ' ' || last_name (exact)", - "date_start": "call.date_start (datetime)", - "description": "call.description (exact)", - "direction": "call.direction (exact)", - "duration_minutes": "call.date_start || ' ' || date_end (minutes)", - "lead": "call.parent_id -> lead.first_name || ' ' || last_name (exact)", - "lead_company": "call.parent_id -> lead.account_name (exact)", - "status": "call.status (exact)", - "subject": "call.name (exact)" - }, - "placed_by_name": [], - "versions": null, - "placed_from_another_kind": [], - "demonstrations": 2, - "children": { - "contact": "call_contact", - "lead#2": "call_lead", - "lead_company#2": "call_lead" - }, - "dispatch_children": {}, - "lists": {}, - "side_rows": [ - { - "table": "call_user", - "count": 1, - "unless": null - } - ], - "logs": [ - "action_history_record", - "auth_log_record" - ], - "log_candidates": [ - "note", - "note_user" - ], - "touched_rows": [ - { - "table": "account", - "selector": { - "name": { - "param": "account" - }, - "billing_address_city": { - "param": "account_city" - } - }, - "columns": { - "stream_updated_at": "timestamp" - }, - "row": "the account row the task's account, account_city name", - "by_key": false - }, - { - "table": "lead", - "selector": { - "account_name": { - "param": "lead_company" - } - }, - "columns": { - "stream_updated_at": "timestamp" - }, - "row": "the lead row the task's lead_company name", - "by_key": false - } - ], - "unexcused": { - "contact": [ - "stream_updated_at" - ] - }, - "refused": [], - "unresolved": {}, - "emitted": 64, - "rejected": [] - } - } -} \ No newline at end of file diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md b/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md deleted file mode 100644 index 4eab26f265..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/redmine/QUESTIONS.md +++ /dev/null @@ -1,16 +0,0 @@ -# Questions for a person - -1. Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (issues: the target row): issues: lock_version, updated_on. Also changed with no task value, and not excused unless you add them under columns_add (a write that changes them stops): user_preferences: others. Rows the app adds beside each write, expected exactly that many times: custom_values (2 per write). Tables that gained a different number of rows in different demonstrations; they are not logs unless you list them under logs_add (rows may then be added, existing rows must not change): journal_details. Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): issues: created_on, description, updated_on; time_entries: created_on, tmonth, tweek, tyear, updated_on. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log. - - Answer key: `bookkeeping`. Proposed: `accept`. - - Answered: `{"logs_add": ["journal_details"], "columns_add": {"user_preferences": ["others"]}}` - -2. How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `. - - Answer key: `recheck_after_s`. Proposed: `300`. - - Answered: `300` - -## Kinds - -- `create_issue` (create, main table `issues`): 26 contract(s). -- `update_issue` (update, main table `issues`): 63 contract(s). -- `log_time` (create, main table `time_entries`): 4 contract(s). -- `ensure_issue` (create, main table `issues`): 2 contract(s). diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml deleted file mode 100644 index 6c588d8197..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/redmine/answers.yaml +++ /dev/null @@ -1,4 +0,0 @@ -bookkeeping: - logs_add: [journal_details] - columns_add: {user_preferences: [others]} -recheck_after_s: 300 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml deleted file mode 100644 index cd03ca2408..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml +++ /dev/null @@ -1,204 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_issue, create, fields assignee, customer_ref, description, environment, priority, project, subject, tracker. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-redmine -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: psycopg2 - sql_connect_args: &id001 - dbname: redmine - host: 127.0.0.1 - port: 9606 - user: oa_reader - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) - AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", - CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", - CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", - CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", - CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" - AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" - AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS - TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CASE WHEN (SELECT - COUNT(*) FROM "users" u1 WHERE CAST(CAST(u1."firstname" AS TEXT) || ' ' || CAST(u1."lastname" - AS TEXT) AS TEXT) = CAST(CAST(a1."firstname" AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) AS - TEXT)) = 1 THEN CAST(a1."firstname" AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) END FROM "users" - a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" AS TEXT)) AS "p__assignee", (SELECT CASE - WHEN (SELECT COUNT(*) FROM "enumerations" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" - AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "enumerations" a1 WHERE CAST(a1."id" - AS TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CASE WHEN (SELECT COUNT(*) - FROM "projects" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) - AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) - = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT CASE WHEN (SELECT COUNT(*) FROM "trackers" - u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) AS TEXT)) = 1 THEN - CAST(a1."name" AS TEXT) END FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" - AS TEXT)) AS "p__tracker", CASE WHEN t."category_id" IS NULL THEN '1' ELSE '0' END AS "pin__category_id", - CASE WHEN t."closed_on" IS NULL THEN '1' ELSE '0' END AS "pin__closed_on", CASE WHEN CAST(t."done_ratio" - AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__done_ratio", CASE WHEN t."estimated_hours" - IS NULL THEN '1' ELSE '0' END AS "pin__estimated_hours", CASE WHEN t."fixed_version_id" IS NULL - THEN '1' ELSE '0' END AS "pin__fixed_version_id", CASE WHEN CAST(CAST(t."is_private" AS integer) - AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__is_private", CASE WHEN CAST(t."lft" - AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__lft", CASE WHEN CAST(t."lock_version" - AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__lock_version", CASE WHEN t."parent_id" - IS NULL THEN '1' ELSE '0' END AS "pin__parent_id", CASE WHEN CAST(t."rgt" AS numeric) = CAST('2' - AS numeric) THEN '1' ELSE '0' END AS "pin__rgt", CASE WHEN t."start_date" IS NULL THEN '1' ELSE - '0' END AS "pin__start_date", CASE WHEN CAST(t."status_id" AS numeric) = CAST('1' AS numeric) - THEN '1' ELSE '0' END AS "pin__status_id", CASE WHEN t."due_date" IS NULL THEN '1' ELSE '0' END - AS "empty__due_date" FROM "issues" t - rows_custom_values: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", - CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "users" u2 - WHERE CAST(CAST(u2."firstname" AS TEXT) || ' ' || CAST(u2."lastname" AS TEXT) AS TEXT) = CAST(CAST(a2."firstname" - AS TEXT) || ' ' || CAST(a2."lastname" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."firstname" AS TEXT) - || ' ' || CAST(a2."lastname" AS TEXT) END FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" - AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", (SELECT CAST(a1."description" - AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__description", (SELECT (SELECT - CASE WHEN (SELECT COUNT(*) FROM "enumerations" u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) - = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" AS TEXT) END FROM "enumerations" - a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."priority_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" - = t."customized_id") AS "main__priority", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" - u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN - CAST(a2."name" AS TEXT) END FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" - AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__project", (SELECT CAST(a1."subject" - AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__subject", (SELECT (SELECT - CASE WHEN (SELECT COUNT(*) FROM "trackers" u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" - AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" AS TEXT) END FROM "trackers" a2 WHERE CAST(a2."id" - AS TEXT) = CAST(a1."tracker_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") - AS "main__tracker", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('1' AS numeric) THEN - '1' ELSE '0' END AS "pin__custom_field_id", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('2' - AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id__47c66b" FROM "custom_values" t - raw_user_preferences: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" - AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) - AS "time_zone" FROM "user_preferences" t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__assignee: - param: assignee - description: - param: description - p__priority: - param: priority - p__project: - param: project - subject: - param: subject - p__tracker: - param: tracker - author_id: - param: bot_user_id - pin__category_id: - literal: '1' - pin__closed_on: - literal: '1' - pin__done_ratio: - literal: '1' - pin__estimated_hours: - literal: '1' - pin__fixed_version_id: - literal: '1' - pin__is_private: - literal: '1' - pin__lft: - literal: '1' - pin__lock_version: - literal: '1' - pin__parent_id: - literal: '1' - pin__rgt: - literal: '1' - pin__start_date: - literal: '1' - pin__status_id: - literal: '1' - empty__due_date: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_custom_values|a row beside the main record holds customer_ref - match: - main__assignee: - param: assignee - main__description: - param: description - main__priority: - param: priority - main__project: - param: project - main__subject: - param: subject - main__tracker: - param: tracker - value: - param: customer_ref - pin__custom_field_id: - literal: '1' - customized_type: - literal: Issue - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=rows_custom_values|a row beside the main record holds environment - match: - main__assignee: - param: assignee - main__description: - param: description - main__priority: - param: priority - main__project: - param: project - main__subject: - param: subject - main__tracker: - param: tracker - value: - param: environment - pin__custom_field_id__47c66b: - literal: '1' - customized_type: - literal: Issue - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app - match: - id: - literal: '1' - expected_count: 1 -frame: - target_fields: - - others - database: - dialect: postgresql - read: - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - allow_tables: - - table: journal_details - - table: custom_values - - table: issues - views: - user_preferences: raw_user_preferences - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml deleted file mode 100644 index bf4588fca2..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml +++ /dev/null @@ -1,196 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind create_issue, found, fields assignee, customer_ref, description, environment, priority, project, subject, tracker. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-redmine -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: psycopg2 - sql_connect_args: &id001 - dbname: redmine - host: 127.0.0.1 - port: 9606 - user: oa_reader - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) - AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", - CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", - CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", - CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", - CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" - AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" - AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS - TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CASE WHEN (SELECT - COUNT(*) FROM "users" u1 WHERE CAST(CAST(u1."firstname" AS TEXT) || ' ' || CAST(u1."lastname" - AS TEXT) AS TEXT) = CAST(CAST(a1."firstname" AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) AS - TEXT)) = 1 THEN CAST(a1."firstname" AS TEXT) || ' ' || CAST(a1."lastname" AS TEXT) END FROM "users" - a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."assigned_to_id" AS TEXT)) AS "p__assignee", (SELECT CASE - WHEN (SELECT COUNT(*) FROM "enumerations" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" - AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "enumerations" a1 WHERE CAST(a1."id" - AS TEXT) = CAST(t."priority_id" AS TEXT)) AS "p__priority", (SELECT CASE WHEN (SELECT COUNT(*) - FROM "projects" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) - AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) - = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT CASE WHEN (SELECT COUNT(*) FROM "trackers" - u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) AS TEXT)) = 1 THEN - CAST(a1."name" AS TEXT) END FROM "trackers" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."tracker_id" - AS TEXT)) AS "p__tracker", CASE WHEN t."category_id" IS NULL THEN '1' ELSE '0' END AS "pin__category_id", - CASE WHEN t."closed_on" IS NULL THEN '1' ELSE '0' END AS "pin__closed_on", CASE WHEN CAST(t."done_ratio" - AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__done_ratio", CASE WHEN t."estimated_hours" - IS NULL THEN '1' ELSE '0' END AS "pin__estimated_hours", CASE WHEN t."fixed_version_id" IS NULL - THEN '1' ELSE '0' END AS "pin__fixed_version_id", CASE WHEN CAST(CAST(t."is_private" AS integer) - AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__is_private", CASE WHEN CAST(t."lft" - AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__lft", CASE WHEN CAST(t."lock_version" - AS numeric) = CAST('1' AS numeric) THEN '1' ELSE '0' END AS "pin__lock_version", CASE WHEN t."parent_id" - IS NULL THEN '1' ELSE '0' END AS "pin__parent_id", CASE WHEN CAST(t."rgt" AS numeric) = CAST('2' - AS numeric) THEN '1' ELSE '0' END AS "pin__rgt", CASE WHEN t."start_date" IS NULL THEN '1' ELSE - '0' END AS "pin__start_date", CASE WHEN CAST(t."status_id" AS numeric) = CAST('1' AS numeric) - THEN '1' ELSE '0' END AS "pin__status_id" FROM "issues" t - rows_custom_values: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."customized_type" AS TEXT) AS "customized_type", - CAST(t."customized_id" AS TEXT) AS "customized_id", CAST(t."custom_field_id" AS TEXT) AS "custom_field_id", - CAST(t."value" AS TEXT) AS "value", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "users" u2 - WHERE CAST(CAST(u2."firstname" AS TEXT) || ' ' || CAST(u2."lastname" AS TEXT) AS TEXT) = CAST(CAST(a2."firstname" - AS TEXT) || ' ' || CAST(a2."lastname" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."firstname" AS TEXT) - || ' ' || CAST(a2."lastname" AS TEXT) END FROM "users" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."assigned_to_id" - AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__assignee", (SELECT CAST(a1."description" - AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__description", (SELECT (SELECT - CASE WHEN (SELECT COUNT(*) FROM "enumerations" u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) - = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" AS TEXT) END FROM "enumerations" - a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."priority_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" - = t."customized_id") AS "main__priority", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" - u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN - CAST(a2."name" AS TEXT) END FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" - AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__project", (SELECT CAST(a1."subject" - AS TEXT) FROM "issues" a1 WHERE a1."id" = t."customized_id") AS "main__subject", (SELECT (SELECT - CASE WHEN (SELECT COUNT(*) FROM "trackers" u2 WHERE CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" - AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" AS TEXT) END FROM "trackers" a2 WHERE CAST(a2."id" - AS TEXT) = CAST(a1."tracker_id" AS TEXT)) FROM "issues" a1 WHERE a1."id" = t."customized_id") - AS "main__tracker", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('1' AS numeric) THEN - '1' ELSE '0' END AS "pin__custom_field_id", CASE WHEN CAST(t."custom_field_id" AS numeric) = CAST('2' - AS numeric) THEN '1' ELSE '0' END AS "pin__custom_field_id__47c66b" FROM "custom_values" t - raw_user_preferences: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" - AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) - AS "time_zone" FROM "user_preferences" t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__assignee: - param: assignee - description: - param: description - p__priority: - param: priority - p__project: - param: project - subject: - param: subject - p__tracker: - param: tracker - pin__category_id: - literal: '1' - pin__closed_on: - literal: '1' - pin__done_ratio: - literal: '1' - pin__estimated_hours: - literal: '1' - pin__fixed_version_id: - literal: '1' - pin__is_private: - literal: '1' - pin__lft: - literal: '1' - pin__lock_version: - literal: '1' - pin__parent_id: - literal: '1' - pin__rgt: - literal: '1' - pin__start_date: - literal: '1' - pin__status_id: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=rows_custom_values|a row beside the main record holds customer_ref - match: - main__assignee: - param: assignee - main__description: - param: description - main__priority: - param: priority - main__project: - param: project - main__subject: - param: subject - main__tracker: - param: tracker - value: - param: customer_ref - pin__custom_field_id: - literal: '1' - customized_type: - literal: Issue - expected_count: 1 -- kind: record_written - probe: surface=rows_custom_values|a row beside the main record holds environment - match: - main__assignee: - param: assignee - main__description: - param: description - main__priority: - param: priority - main__project: - param: project - main__subject: - param: subject - main__tracker: - param: tracker - value: - param: environment - pin__custom_field_id__47c66b: - literal: '1' - customized_type: - literal: Issue - expected_count: 1 -- kind: record_written - probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app - match: - id: - literal: '1' - expected_count: 1 -frame: - target_fields: - - others - database: - dialect: postgresql - read: - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - allow_tables: - - table: journal_details - - table: custom_values - - table: issues - views: - user_preferences: raw_user_preferences - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml deleted file mode 100644 index ac628cdc8e..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml +++ /dev/null @@ -1,92 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind log_time, create, fields activity, comments, hours, issue_subject, project, spent_on. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-redmine -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: psycopg2 - sql_connect_args: &id001 - dbname: redmine - host: 127.0.0.1 - port: 9606 - user: oa_reader - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", - CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" - AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", - CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CASE WHEN (SELECT - COUNT(*) FROM "enumerations" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" - AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "enumerations" a1 WHERE CAST(a1."id" - AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", CASE WHEN CAST(t."hours" AS numeric) - = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", (SELECT CASE WHEN (SELECT COUNT(*) - FROM "issues" u1 WHERE CAST(CAST(u1."subject" AS TEXT) AS TEXT) = CAST(CAST(a1."subject" AS TEXT) - AS TEXT)) = 1 THEN CAST(a1."subject" AS TEXT) END FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) - = CAST(t."issue_id" AS TEXT)) AS "p__issue_subject", (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" - u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) AS TEXT)) = 1 THEN - CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" - AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END - AS "p__spent_on" FROM "time_entries" t - sql_query_params: - v0: - param: hours - v1: - param: spent_on - raw_user_preferences: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" - AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) - AS "time_zone" FROM "user_preferences" t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__activity: - param: activity - comments: - param: comments - p__hours: - literal: '1' - p__issue_subject: - param: issue_subject - p__project: - param: project - p__spent_on: - literal: '1' - author_id: - param: bot_user_id - user_id: - param: bot_user_id - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app - match: - id: - literal: '1' - expected_count: 1 -frame: - target_fields: - - others - database: - dialect: postgresql - read: - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - allow_tables: - - table: journal_details - - table: time_entries - views: - user_preferences: raw_user_preferences - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml deleted file mode 100644 index 666e1672db..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml +++ /dev/null @@ -1,87 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind log_time, found, fields activity, comments, hours, issue_subject, project, spent_on. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-redmine -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: psycopg2 - sql_connect_args: &id001 - dbname: redmine - host: 127.0.0.1 - port: 9606 - user: oa_reader - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."project_id" AS TEXT) AS "project_id", CAST(t."user_id" - AS TEXT) AS "user_id", CAST(t."issue_id" AS TEXT) AS "issue_id", CAST(t."hours" AS TEXT) AS "hours", - CAST(t."comments" AS TEXT) AS "comments", CAST(t."activity_id" AS TEXT) AS "activity_id", CAST(t."spent_on" - AS TEXT) AS "spent_on", CAST(t."tyear" AS TEXT) AS "tyear", CAST(t."tmonth" AS TEXT) AS "tmonth", - CAST(t."tweek" AS TEXT) AS "tweek", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" - AS TEXT) AS "updated_on", CAST(t."author_id" AS TEXT) AS "author_id", (SELECT CASE WHEN (SELECT - COUNT(*) FROM "enumerations" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" - AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "enumerations" a1 WHERE CAST(a1."id" - AS TEXT) = CAST(t."activity_id" AS TEXT)) AS "p__activity", CASE WHEN CAST(t."hours" AS numeric) - = CAST(%(v0)s AS numeric) THEN '1' ELSE '0' END AS "p__hours", (SELECT CASE WHEN (SELECT COUNT(*) - FROM "issues" u1 WHERE CAST(CAST(u1."subject" AS TEXT) AS TEXT) = CAST(CAST(a1."subject" AS TEXT) - AS TEXT)) = 1 THEN CAST(a1."subject" AS TEXT) END FROM "issues" a1 WHERE CAST(a1."id" AS TEXT) - = CAST(t."issue_id" AS TEXT)) AS "p__issue_subject", (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" - u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) AS TEXT)) = 1 THEN - CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" AS TEXT) = CAST(t."project_id" - AS TEXT)) AS "p__project", CASE WHEN t."spent_on" = CAST(%(v1)s AS date) THEN '1' ELSE '0' END - AS "p__spent_on" FROM "time_entries" t - sql_query_params: - v0: - param: hours - v1: - param: spent_on - raw_user_preferences: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" - AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) - AS "time_zone" FROM "user_preferences" t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__activity: - param: activity - comments: - param: comments - p__hours: - literal: '1' - p__issue_subject: - param: issue_subject - p__project: - param: project - p__spent_on: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app - match: - id: - literal: '1' - expected_count: 1 -frame: - target_fields: - - others - database: - dialect: postgresql - read: - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - allow_tables: - - table: journal_details - - table: time_entries - views: - user_preferences: raw_user_preferences - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml deleted file mode 100644 index 8646bc39d6..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml +++ /dev/null @@ -1,126 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind update_issue, found, fields notes, project, status, subject. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-redmine -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: psycopg2 - sql_connect_args: &id001 - dbname: redmine - host: 127.0.0.1 - port: 9606 - user: oa_reader - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) - AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", - CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", - CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", - CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", - CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" - AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" - AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS - TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CASE WHEN (SELECT - COUNT(*) FROM "projects" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" - AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" - AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT CASE WHEN (SELECT COUNT(*) FROM - "issue_statuses" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) - AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "issue_statuses" a1 WHERE CAST(a1."id" AS - TEXT) = CAST(t."status_id" AS TEXT)) AS "p__status" FROM "issues" t - rows_journals: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", - CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" - AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" - AS TEXT) AS "updated_by_id", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" u2 WHERE - CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" - AS TEXT) END FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM - "issues" a1 WHERE a1."id" = t."journalized_id") AS "main__project", (SELECT CAST(a1."subject" - AS TEXT) FROM "issues" a1 WHERE a1."id" = t."journalized_id") AS "main__subject", CASE WHEN CAST(CAST(t."private_notes" - AS integer) AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__private_notes", - CASE WHEN t."updated_by_id" IS NULL THEN '1' ELSE '0' END AS "pin__updated_by_id", CASE WHEN CAST(t."user_id" - AS numeric) = CAST('5' AS numeric) THEN '1' ELSE '0' END AS "pin__user_id" FROM "journals" t - raw_user_preferences: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" - AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) - AS "time_zone" FROM "user_preferences" t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__project: - param: project - p__status: - param: status - subject: - param: subject - expected_count: 1 -- kind: record_written - probe: surface=rows_journals|a row beside the main record holds notes - match: - main__project: - param: project - main__subject: - param: subject - notes: - param: notes - journalized_type: - literal: Issue - pin__private_notes: - literal: '1' - pin__updated_by_id: - literal: '1' - pin__user_id: - literal: '1' - expected_count: 1 -- kind: record_written - probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app - match: - id: - literal: '1' - expected_count: 1 -frame: - target_fields: - - others - versions: - read: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."journalized_id" AS TEXT) AS "id", CAST(t."id" AS TEXT) AS "ver", CAST(t."user_id" - AS TEXT) AS "author" FROM "journals" t - record_field: id - target_field: id - version_field: ver - author_field: author - self: - - param: bot_user_id - database: - dialect: postgresql - read: - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - allow_tables: - - table: journal_details - - table: issues - - table: journals - views: - user_preferences: raw_user_preferences - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml deleted file mode 100644 index 0cc306597c..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml +++ /dev/null @@ -1,135 +0,0 @@ -# Generated by openadapt_flow.generate.verify_contract: kind update_issue, update, fields notes, project, status, subject. -# Proposed, not admitted: a person answers QUESTIONS.md first. -schema_version: openadapt.verify/v1 -environment_id: generated-redmine -max_execution_window_s: 900 -verifier: - kind: surfaces - surfaces: - record: - kind: sql - sql_driver: psycopg2 - sql_connect_args: &id001 - dbname: redmine - host: 127.0.0.1 - port: 9606 - user: oa_reader - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."tracker_id" AS TEXT) AS "tracker_id", CAST(t."project_id" - AS TEXT) AS "project_id", CAST(t."subject" AS TEXT) AS "subject", CAST(t."description" AS TEXT) - AS "description", CAST(t."due_date" AS TEXT) AS "due_date", CAST(t."category_id" AS TEXT) AS "category_id", - CAST(t."status_id" AS TEXT) AS "status_id", CAST(t."assigned_to_id" AS TEXT) AS "assigned_to_id", - CAST(t."priority_id" AS TEXT) AS "priority_id", CAST(t."fixed_version_id" AS TEXT) AS "fixed_version_id", - CAST(t."author_id" AS TEXT) AS "author_id", CAST(t."lock_version" AS TEXT) AS "lock_version", - CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."start_date" - AS TEXT) AS "start_date", CAST(t."done_ratio" AS TEXT) AS "done_ratio", CAST(t."estimated_hours" - AS TEXT) AS "estimated_hours", CAST(t."parent_id" AS TEXT) AS "parent_id", CAST(t."root_id" AS - TEXT) AS "root_id", CAST(t."lft" AS TEXT) AS "lft", CAST(t."rgt" AS TEXT) AS "rgt", CAST(t."is_private" - AS TEXT) AS "is_private", CAST(t."closed_on" AS TEXT) AS "closed_on", (SELECT CASE WHEN (SELECT - COUNT(*) FROM "projects" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" - AS TEXT) AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "projects" a1 WHERE CAST(a1."id" - AS TEXT) = CAST(t."project_id" AS TEXT)) AS "p__project", (SELECT CASE WHEN (SELECT COUNT(*) FROM - "issue_statuses" u1 WHERE CAST(CAST(u1."name" AS TEXT) AS TEXT) = CAST(CAST(a1."name" AS TEXT) - AS TEXT)) = 1 THEN CAST(a1."name" AS TEXT) END FROM "issue_statuses" a1 WHERE CAST(a1."id" AS - TEXT) = CAST(t."status_id" AS TEXT)) AS "p__status" FROM "issues" t - rows_journals: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."journalized_id" AS TEXT) AS "journalized_id", - CAST(t."journalized_type" AS TEXT) AS "journalized_type", CAST(t."user_id" AS TEXT) AS "user_id", - CAST(t."notes" AS TEXT) AS "notes", CAST(t."created_on" AS TEXT) AS "created_on", CAST(t."private_notes" - AS TEXT) AS "private_notes", CAST(t."updated_on" AS TEXT) AS "updated_on", CAST(t."updated_by_id" - AS TEXT) AS "updated_by_id", (SELECT (SELECT CASE WHEN (SELECT COUNT(*) FROM "projects" u2 WHERE - CAST(CAST(u2."name" AS TEXT) AS TEXT) = CAST(CAST(a2."name" AS TEXT) AS TEXT)) = 1 THEN CAST(a2."name" - AS TEXT) END FROM "projects" a2 WHERE CAST(a2."id" AS TEXT) = CAST(a1."project_id" AS TEXT)) FROM - "issues" a1 WHERE a1."id" = t."journalized_id") AS "main__project", (SELECT CAST(a1."subject" - AS TEXT) FROM "issues" a1 WHERE a1."id" = t."journalized_id") AS "main__subject", CASE WHEN CAST(CAST(t."private_notes" - AS integer) AS numeric) = CAST('0' AS numeric) THEN '1' ELSE '0' END AS "pin__private_notes", - CASE WHEN t."updated_by_id" IS NULL THEN '1' ELSE '0' END AS "pin__updated_by_id", CASE WHEN CAST(t."user_id" - AS numeric) = CAST('5' AS numeric) THEN '1' ELSE '0' END AS "pin__user_id" FROM "journals" t - raw_user_preferences: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."id" AS TEXT) AS "id", CAST(t."user_id" AS TEXT) AS "user_id", CAST(t."others" - AS TEXT) AS "others", CAST(t."hide_mail" AS TEXT) AS "hide_mail", CAST(t."time_zone" AS TEXT) - AS "time_zone" FROM "user_preferences" t -effects: -- kind: record_written - probe: surface=record|the main record holds the task's values - match: - p__project: - param: project - p__status: - param: status - subject: - param: subject - expected_count: 1 -- kind: record_written - probe: surface=rows_journals|a row beside the main record holds notes - match: - main__project: - param: project - main__subject: - param: subject - notes: - param: notes - journalized_type: - literal: Issue - pin__private_notes: - literal: '1' - pin__updated_by_id: - literal: '1' - pin__user_id: - literal: '1' - expected_count: 1 - count_new_only: true -- kind: record_written - probe: surface=raw_user_preferences|user_preferences row id=1, changed by the app - match: - id: - literal: '1' - expected_count: 1 -frame: - target_fields: - - lock_version - - others - - p__status - - status_id - - updated_on - versions: - read: - kind: sql - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - sql_query: SELECT CAST(t."journalized_id" AS TEXT) AS "id", CAST(t."id" AS TEXT) AS "ver", CAST(t."user_id" - AS TEXT) AS "author" FROM "journals" t - record_field: id - target_field: id - version_field: ver - author_field: author - self: - - param: bot_user_id - database: - dialect: postgresql - read: - sql_driver: psycopg2 - sql_connect_args: *id001 - sql_password_env: REDMINE_RO_DB_PASSWORD - allow_tables: - - table: journal_details - - table: issues - mode: skip - reason: read in full, every column, by this contract's own surface - - table: journals - mode: skip - reason: read in full, every column, by this contract's own surface - views: - user_preferences: raw_user_preferences - settle_s: 1 - settle_attempts: 5 - recheck_after_s: 300.0 diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml b/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml deleted file mode 100644 index 6249f3d711..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/redmine/manifest.yaml +++ /dev/null @@ -1,1052 +0,0 @@ -schema_version: openadapt.contract-manifest/v1 -dialect: postgresql -database: redmine -actor_params: -- bot_user_id -questions_open: [] -kinds: - create_issue: - mode: create - main_table: issues - parameters: - - project - - tracker - - subject - - priority - - assignee - - due_date - - description - - customer_ref - - environment - identity: [] - lists: [] - dispatch: null - variants: - - fields: - - assignee - - due_date - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+due_date+priority+project+subject+tracker.cb8524d8.create.yaml - found: contracts/create_issue/assignee+due_date+priority+project+subject+tracker.cb8524d8.found.yaml - - fields: - - assignee - - customer_ref - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.create.yaml - found: contracts/create_issue/assignee+customer_ref+environment+priority+project+subject+tracker.1fa8ca93.found.yaml - - fields: - - due_date - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.create.yaml - found: contracts/create_issue/due_date+priority+project+subject+tracker.57764031.found.yaml - - fields: - - assignee - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.create.yaml - found: contracts/create_issue/assignee+priority+project+subject+tracker.d16f0df9.found.yaml - - fields: - - customer_ref - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/customer_ref+environment+priority+project+subject+tracker.e94e6033.create.yaml - found: contracts/create_issue/customer_ref+environment+priority+project+subject+tracker.e94e6033.found.yaml - - fields: - - assignee - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+environment+priority+project+subject+tracker.b1832d09.create.yaml - found: contracts/create_issue/assignee+environment+priority+project+subject+tracker.b1832d09.found.yaml - - fields: - - assignee - - customer_ref - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+priority+project+subject+tracker.88f7523d.create.yaml - found: contracts/create_issue/assignee+customer_ref+priority+project+subject+tracker.88f7523d.found.yaml - - fields: - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/priority+project+subject+tracker.a0dc8790.create.yaml - found: contracts/create_issue/priority+project+subject+tracker.a0dc8790.found.yaml - - fields: - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/environment+priority+project+subject+tracker.99d89352.create.yaml - found: contracts/create_issue/environment+priority+project+subject+tracker.99d89352.found.yaml - - fields: - - customer_ref - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/customer_ref+priority+project+subject+tracker.322376d5.create.yaml - found: contracts/create_issue/customer_ref+priority+project+subject+tracker.322376d5.found.yaml - - fields: - - assignee - - customer_ref - - due_date - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+due_date+priority+project+subject+tracker.f7bf10f1.create.yaml - found: contracts/create_issue/assignee+customer_ref+due_date+priority+project+subject+tracker.f7bf10f1.found.yaml - - fields: - - assignee - - description - - due_date - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+description+due_date+priority+project+subject+tracker.3045a6ca.create.yaml - found: contracts/create_issue/assignee+description+due_date+priority+project+subject+tracker.3045a6ca.found.yaml - - fields: - - assignee - - due_date - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+due_date+environment+priority+project+subject+tracker.9f2faa3a.create.yaml - found: contracts/create_issue/assignee+due_date+environment+priority+project+subject+tracker.9f2faa3a.found.yaml - - fields: - - assignee - - customer_ref - - description - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml - found: contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml - - fields: - - assignee - - customer_ref - - due_date - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.create.yaml - found: contracts/create_issue/assignee+customer_ref+due_date+environment+priority+project+subject+tracker.47c18f25.found.yaml - - fields: - - customer_ref - - due_date - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.create.yaml - found: contracts/create_issue/customer_ref+due_date+priority+project+subject+tracker.8cd341cf.found.yaml - - fields: - - description - - due_date - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.create.yaml - found: contracts/create_issue/description+due_date+priority+project+subject+tracker.7d7c484f.found.yaml - - fields: - - due_date - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.create.yaml - found: contracts/create_issue/due_date+environment+priority+project+subject+tracker.e7588ee6.found.yaml - - fields: - - assignee - - description - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.create.yaml - found: contracts/create_issue/assignee+description+priority+project+subject+tracker.054b82b6.found.yaml - - fields: - - customer_ref - - description - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/customer_ref+description+environment+priority+project+subject+tracker.8bd3a0b3.create.yaml - found: contracts/create_issue/customer_ref+description+environment+priority+project+subject+tracker.8bd3a0b3.found.yaml - - fields: - - customer_ref - - due_date - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/customer_ref+due_date+environment+priority+project+subject+tracker.018e5a55.create.yaml - found: contracts/create_issue/customer_ref+due_date+environment+priority+project+subject+tracker.018e5a55.found.yaml - - fields: - - assignee - - description - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+description+environment+priority+project+subject+tracker.1563d2ca.create.yaml - found: contracts/create_issue/assignee+description+environment+priority+project+subject+tracker.1563d2ca.found.yaml - - fields: - - assignee - - customer_ref - - description - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/assignee+customer_ref+description+priority+project+subject+tracker.4d3aeb69.create.yaml - found: contracts/create_issue/assignee+customer_ref+description+priority+project+subject+tracker.4d3aeb69.found.yaml - - fields: - - description - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.create.yaml - found: contracts/create_issue/description+priority+project+subject+tracker.ec0f6d4d.found.yaml - - fields: - - description - - environment - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/description+environment+priority+project+subject+tracker.4203fe30.create.yaml - found: contracts/create_issue/description+environment+priority+project+subject+tracker.4203fe30.found.yaml - - fields: - - customer_ref - - description - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/create_issue/customer_ref+description+priority+project+subject+tracker.c483a119.create.yaml - found: contracts/create_issue/customer_ref+description+priority+project+subject+tracker.c483a119.found.yaml - update_issue: - mode: update - main_table: issues - parameters: - - project - - subject - - status - - assignee - - priority - - due_date - - done_ratio - - notes - identity: - - project - - subject - lists: [] - dispatch: null - variants: - - fields: - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/project+status+subject.f5cf2619.update.yaml - found: contracts/update_issue/project+status+subject.f5cf2619.found.yaml - - fields: - - assignee - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+project+subject.4e643112.update.yaml - found: contracts/update_issue/assignee+project+subject.4e643112.found.yaml - - fields: - - due_date - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/due_date+project+subject.4f8546da.update.yaml - found: contracts/update_issue/due_date+project+subject.4f8546da.found.yaml - - fields: - - done_ratio - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+project+subject.5f3c3fa0.update.yaml - found: contracts/update_issue/done_ratio+project+subject.5f3c3fa0.found.yaml - - fields: - - notes - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/notes+project+subject.05b2ea74.update.yaml - found: contracts/update_issue/notes+project+subject.05b2ea74.found.yaml - - fields: - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/priority+project+subject.a57fbe91.update.yaml - found: contracts/update_issue/priority+project+subject.a57fbe91.found.yaml - - fields: - - assignee - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+project+status+subject.a05db09f.update.yaml - found: contracts/update_issue/assignee+project+status+subject.a05db09f.found.yaml - - fields: - - due_date - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/due_date+project+status+subject.0af96bde.update.yaml - found: contracts/update_issue/due_date+project+status+subject.0af96bde.found.yaml - - fields: - - done_ratio - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+project+status+subject.474ffed4.update.yaml - found: contracts/update_issue/done_ratio+project+status+subject.474ffed4.found.yaml - - fields: - - notes - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml - found: contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml - - fields: - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/priority+project+status+subject.bbe05441.update.yaml - found: contracts/update_issue/priority+project+status+subject.bbe05441.found.yaml - - fields: - - assignee - - due_date - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+due_date+project+subject.159f21e0.update.yaml - found: contracts/update_issue/assignee+due_date+project+subject.159f21e0.found.yaml - - fields: - - assignee - - done_ratio - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+project+subject.5df67a01.update.yaml - found: contracts/update_issue/assignee+done_ratio+project+subject.5df67a01.found.yaml - - fields: - - assignee - - notes - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+notes+project+subject.bbff0247.update.yaml - found: contracts/update_issue/assignee+notes+project+subject.bbff0247.found.yaml - - fields: - - assignee - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+priority+project+subject.b5e14879.update.yaml - found: contracts/update_issue/assignee+priority+project+subject.b5e14879.found.yaml - - fields: - - done_ratio - - due_date - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+due_date+project+subject.9d543b40.update.yaml - found: contracts/update_issue/done_ratio+due_date+project+subject.9d543b40.found.yaml - - fields: - - due_date - - notes - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/due_date+notes+project+subject.6f69bb62.update.yaml - found: contracts/update_issue/due_date+notes+project+subject.6f69bb62.found.yaml - - fields: - - due_date - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/due_date+priority+project+subject.119bebbc.update.yaml - found: contracts/update_issue/due_date+priority+project+subject.119bebbc.found.yaml - - fields: - - done_ratio - - notes - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+notes+project+subject.42cc7615.update.yaml - found: contracts/update_issue/done_ratio+notes+project+subject.42cc7615.found.yaml - - fields: - - done_ratio - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+priority+project+subject.3f024f0d.update.yaml - found: contracts/update_issue/done_ratio+priority+project+subject.3f024f0d.found.yaml - - fields: - - notes - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/notes+priority+project+subject.8682b0ac.update.yaml - found: contracts/update_issue/notes+priority+project+subject.8682b0ac.found.yaml - - fields: - - assignee - - due_date - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+due_date+project+status+subject.c3a69cb4.update.yaml - found: contracts/update_issue/assignee+due_date+project+status+subject.c3a69cb4.found.yaml - - fields: - - assignee - - done_ratio - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+project+status+subject.54276404.update.yaml - found: contracts/update_issue/assignee+done_ratio+project+status+subject.54276404.found.yaml - - fields: - - assignee - - notes - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+notes+project+status+subject.c7d221b1.update.yaml - found: contracts/update_issue/assignee+notes+project+status+subject.c7d221b1.found.yaml - - fields: - - assignee - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+priority+project+status+subject.bc85aafd.update.yaml - found: contracts/update_issue/assignee+priority+project+status+subject.bc85aafd.found.yaml - - fields: - - done_ratio - - due_date - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+due_date+project+status+subject.46328550.update.yaml - found: contracts/update_issue/done_ratio+due_date+project+status+subject.46328550.found.yaml - - fields: - - due_date - - notes - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/due_date+notes+project+status+subject.73fff321.update.yaml - found: contracts/update_issue/due_date+notes+project+status+subject.73fff321.found.yaml - - fields: - - due_date - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/due_date+priority+project+status+subject.6461d8ca.update.yaml - found: contracts/update_issue/due_date+priority+project+status+subject.6461d8ca.found.yaml - - fields: - - done_ratio - - notes - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+notes+project+status+subject.aec2705d.update.yaml - found: contracts/update_issue/done_ratio+notes+project+status+subject.aec2705d.found.yaml - - fields: - - done_ratio - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+priority+project+status+subject.5b1b1f40.update.yaml - found: contracts/update_issue/done_ratio+priority+project+status+subject.5b1b1f40.found.yaml - - fields: - - notes - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/notes+priority+project+status+subject.a5a6c598.update.yaml - found: contracts/update_issue/notes+priority+project+status+subject.a5a6c598.found.yaml - - fields: - - assignee - - done_ratio - - due_date - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+due_date+project+subject.e06f0a37.update.yaml - found: contracts/update_issue/assignee+done_ratio+due_date+project+subject.e06f0a37.found.yaml - - fields: - - assignee - - due_date - - notes - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+due_date+notes+project+subject.177404ca.update.yaml - found: contracts/update_issue/assignee+due_date+notes+project+subject.177404ca.found.yaml - - fields: - - assignee - - due_date - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+due_date+priority+project+subject.bed7fd8a.update.yaml - found: contracts/update_issue/assignee+due_date+priority+project+subject.bed7fd8a.found.yaml - - fields: - - assignee - - done_ratio - - notes - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+notes+project+subject.7054e59a.update.yaml - found: contracts/update_issue/assignee+done_ratio+notes+project+subject.7054e59a.found.yaml - - fields: - - assignee - - done_ratio - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+priority+project+subject.887e7284.update.yaml - found: contracts/update_issue/assignee+done_ratio+priority+project+subject.887e7284.found.yaml - - fields: - - assignee - - notes - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+notes+priority+project+subject.c8e8610c.update.yaml - found: contracts/update_issue/assignee+notes+priority+project+subject.c8e8610c.found.yaml - - fields: - - done_ratio - - due_date - - notes - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+due_date+notes+project+subject.036ba8cb.update.yaml - found: contracts/update_issue/done_ratio+due_date+notes+project+subject.036ba8cb.found.yaml - - fields: - - done_ratio - - due_date - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+due_date+priority+project+subject.530b95ee.update.yaml - found: contracts/update_issue/done_ratio+due_date+priority+project+subject.530b95ee.found.yaml - - fields: - - due_date - - notes - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/due_date+notes+priority+project+subject.185b2051.update.yaml - found: contracts/update_issue/due_date+notes+priority+project+subject.185b2051.found.yaml - - fields: - - done_ratio - - notes - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+notes+priority+project+subject.3b999ec2.update.yaml - found: contracts/update_issue/done_ratio+notes+priority+project+subject.3b999ec2.found.yaml - - fields: - - assignee - - done_ratio - - due_date - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+due_date+project+status+subject.80ad363e.update.yaml - found: contracts/update_issue/assignee+done_ratio+due_date+project+status+subject.80ad363e.found.yaml - - fields: - - assignee - - due_date - - notes - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+due_date+notes+project+status+subject.07561ad0.update.yaml - found: contracts/update_issue/assignee+due_date+notes+project+status+subject.07561ad0.found.yaml - - fields: - - assignee - - due_date - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+due_date+priority+project+status+subject.9b95d736.update.yaml - found: contracts/update_issue/assignee+due_date+priority+project+status+subject.9b95d736.found.yaml - - fields: - - assignee - - done_ratio - - notes - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+notes+project+status+subject.b2c7c267.update.yaml - found: contracts/update_issue/assignee+done_ratio+notes+project+status+subject.b2c7c267.found.yaml - - fields: - - assignee - - done_ratio - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+priority+project+status+subject.276a5a30.update.yaml - found: contracts/update_issue/assignee+done_ratio+priority+project+status+subject.276a5a30.found.yaml - - fields: - - assignee - - notes - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+notes+priority+project+status+subject.d1cc724b.update.yaml - found: contracts/update_issue/assignee+notes+priority+project+status+subject.d1cc724b.found.yaml - - fields: - - done_ratio - - due_date - - notes - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+due_date+notes+project+status+subject.f2171a53.update.yaml - found: contracts/update_issue/done_ratio+due_date+notes+project+status+subject.f2171a53.found.yaml - - fields: - - done_ratio - - due_date - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+due_date+priority+project+status+subject.14f98761.update.yaml - found: contracts/update_issue/done_ratio+due_date+priority+project+status+subject.14f98761.found.yaml - - fields: - - due_date - - notes - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/due_date+notes+priority+project+status+subject.29f05bb8.update.yaml - found: contracts/update_issue/due_date+notes+priority+project+status+subject.29f05bb8.found.yaml - - fields: - - done_ratio - - notes - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+notes+priority+project+status+subject.eec8202f.update.yaml - found: contracts/update_issue/done_ratio+notes+priority+project+status+subject.eec8202f.found.yaml - - fields: - - assignee - - done_ratio - - due_date - - notes - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+due_date+notes+project+subject.7ff66a52.update.yaml - found: contracts/update_issue/assignee+done_ratio+due_date+notes+project+subject.7ff66a52.found.yaml - - fields: - - assignee - - done_ratio - - due_date - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+due_date+priority+project+subject.6eb0e43c.update.yaml - found: contracts/update_issue/assignee+done_ratio+due_date+priority+project+subject.6eb0e43c.found.yaml - - fields: - - assignee - - due_date - - notes - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+due_date+notes+priority+project+subject.88ebb1f9.update.yaml - found: contracts/update_issue/assignee+due_date+notes+priority+project+subject.88ebb1f9.found.yaml - - fields: - - assignee - - done_ratio - - notes - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+notes+priority+project+subject.4153e619.update.yaml - found: contracts/update_issue/assignee+done_ratio+notes+priority+project+subject.4153e619.found.yaml - - fields: - - done_ratio - - due_date - - notes - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+due_date+notes+priority+project+subject.8da31395.update.yaml - found: contracts/update_issue/done_ratio+due_date+notes+priority+project+subject.8da31395.found.yaml - - fields: - - assignee - - done_ratio - - due_date - - notes - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+due_date+notes+project+status+subject.02a8e039.update.yaml - found: contracts/update_issue/assignee+done_ratio+due_date+notes+project+status+subject.02a8e039.found.yaml - - fields: - - assignee - - done_ratio - - due_date - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+due_date+priority+project+status+subject.113ce920.update.yaml - found: contracts/update_issue/assignee+done_ratio+due_date+priority+project+status+subject.113ce920.found.yaml - - fields: - - assignee - - due_date - - notes - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+due_date+notes+priority+project+status+subject.6ea08e8b.update.yaml - found: contracts/update_issue/assignee+due_date+notes+priority+project+status+subject.6ea08e8b.found.yaml - - fields: - - assignee - - done_ratio - - notes - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+notes+priority+project+status+subject.31317326.update.yaml - found: contracts/update_issue/assignee+done_ratio+notes+priority+project+status+subject.31317326.found.yaml - - fields: - - done_ratio - - due_date - - notes - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/done_ratio+due_date+notes+priority+project+status+subject.3cf03089.update.yaml - found: contracts/update_issue/done_ratio+due_date+notes+priority+project+status+subject.3cf03089.found.yaml - - fields: - - assignee - - done_ratio - - due_date - - notes - - priority - - project - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+subject.514a3f86.update.yaml - found: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+subject.514a3f86.found.yaml - - fields: - - assignee - - done_ratio - - due_date - - notes - - priority - - project - - status - - subject - lists: {} - dispatch_value: null - update: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+status+subject.2d1635fd.update.yaml - found: contracts/update_issue/assignee+done_ratio+due_date+notes+priority+project+status+subject.2d1635fd.found.yaml - log_time: - mode: create - main_table: time_entries - parameters: - - project - - issue_subject - - hours - - spent_on - - activity - - comments - identity: [] - lists: [] - dispatch: null - variants: - - fields: - - activity - - comments - - hours - - issue_subject - - project - - spent_on - lists: {} - dispatch_value: null - create: contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml - found: contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml - - fields: - - activity - - hours - - issue_subject - - project - - spent_on - lists: {} - dispatch_value: null - create: contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.create.yaml - found: contracts/log_time/activity+hours+issue_subject+project+spent_on.d1e5805e.found.yaml - - fields: - - activity - - comments - - hours - - project - - spent_on - lists: {} - dispatch_value: null - create: contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.create.yaml - found: contracts/log_time/activity+comments+hours+project+spent_on.c6f70a8e.found.yaml - - fields: - - activity - - hours - - project - - spent_on - lists: {} - dispatch_value: null - create: contracts/log_time/activity+hours+project+spent_on.e07feee9.create.yaml - found: contracts/log_time/activity+hours+project+spent_on.e07feee9.found.yaml - ensure_issue: - mode: create - main_table: issues - parameters: - - project - - tracker - - subject - - priority - - assignee - identity: [] - lists: [] - dispatch: null - variants: - - fields: - - assignee - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/ensure_issue/assignee+priority+project+subject+tracker.d16f0df9.create.yaml - found: contracts/ensure_issue/assignee+priority+project+subject+tracker.d16f0df9.found.yaml - - fields: - - priority - - project - - subject - - tracker - lists: {} - dispatch_value: null - create: contracts/ensure_issue/priority+project+subject+tracker.a0dc8790.create.yaml - found: contracts/ensure_issue/priority+project+subject+tracker.a0dc8790.found.yaml diff --git a/packages/flow/benchmark/generate_contracts/results/redmine/report.json b/packages/flow/benchmark/generate_contracts/results/redmine/report.json deleted file mode 100644 index 295e7829de..0000000000 --- a/packages/flow/benchmark/generate_contracts/results/redmine/report.json +++ /dev/null @@ -1,231 +0,0 @@ -{ - "applied": [], - "read_account": [], - "dialect": "postgresql", - "tables": 58, - "questions": [ - { - "number": 1, - "key": "bookkeeping", - "text": "Is this the app's own bookkeeping? Proposed as the app's own bookkeeping, excused only on the rows the demonstrations changed (issues: the target row): issues: lock_version, updated_on. Also changed with no task value, and not excused unless you add them under columns_add (a write that changes them stops): user_preferences: others. Rows the app adds beside each write, expected exactly that many times: custom_values (2 per write). Tables that gained a different number of rows in different demonstrations; they are not logs unless you list them under logs_add (rows may then be added, existing rows must not change): journal_details. Columns of a new record that are not checked (they differ between demonstrations, are free text every demonstration left empty, or are the write's time stamp): issues: created_on, description, updated_on; time_entries: created_on, tmonth, tweek, tyear, updated_on. Answer `bookkeeping: accept`, or a mapping with any of logs_add, columns_add, columns_remove. Unanswered, nothing is excused and no table is a log.", - "proposal": "accept", - "answered": true - }, - { - "number": 2, - "key": "recheck_after_s", - "text": "How long after a verified write can the app still write on its own (a background job, a delayed retry)? The database is read again that many seconds later and a late change refutes the write. Proposed: 300. Answer `recheck_after_s: `.", - "proposal": 300, - "answered": true - } - ], - "kinds": { - "create_issue": { - "mode": "create", - "main_table": "issues", - "locations": { - "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", - "customer_ref": "custom_values (whose customized_id is the issues key).value (exact)", - "description": "issues.description (exact)", - "due_date": "issues.due_date (date)", - "environment": "custom_values (whose customized_id is the issues key).value (exact)", - "priority": "issues.priority_id -> enumerations.name (exact)", - "project": "issues.project_id -> projects.name (exact)", - "subject": "issues.subject (exact)", - "tracker": "issues.tracker_id -> trackers.name (exact)" - }, - "placed_by_name": [ - "description" - ], - "versions": null, - "placed_from_another_kind": [], - "demonstrations": 2, - "children": { - "customer_ref": "custom_values", - "environment": "custom_values" - }, - "dispatch_children": {}, - "lists": {}, - "side_rows": [], - "logs": [ - "journal_details" - ], - "log_candidates": [], - "touched_rows": [ - { - "table": "user_preferences", - "selector": { - "id": { - "literal": "1" - } - }, - "columns": { - "others": "other" - }, - "row": "user_preferences row id=1", - "by_key": true - } - ], - "unexcused": {}, - "refused": [], - "unresolved": {}, - "emitted": 26, - "rejected": [] - }, - "update_issue": { - "mode": "update", - "main_table": "issues", - "locations": { - "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", - "done_ratio": "issues.done_ratio (number)", - "due_date": "issues.due_date (date)", - "notes": "journals (whose journalized_id is the issues key).notes (exact)", - "priority": "issues.priority_id -> enumerations.name (exact)", - "project": "issues.project_id -> projects.name (exact)", - "status": "issues.status_id -> issue_statuses.name (exact)", - "subject": "issues.subject (exact)" - }, - "placed_by_name": [ - "done_ratio" - ], - "versions": { - "kind": "audit", - "table": "journals", - "record_column": "journalized_id", - "author_column": "user_id", - "actor_param": "bot_user_id", - "where": {} - }, - "placed_from_another_kind": [ - "due_date", - "priority" - ], - "demonstrations": 2, - "children": { - "notes": "journals" - }, - "dispatch_children": {}, - "lists": {}, - "side_rows": [], - "logs": [ - "journal_details" - ], - "log_candidates": [ - "journal_details" - ], - "touched_rows": [ - { - "table": "user_preferences", - "selector": { - "id": { - "literal": "1" - } - }, - "columns": { - "others": "other" - }, - "row": "user_preferences row id=1", - "by_key": true - } - ], - "unexcused": {}, - "refused": [], - "unresolved": {}, - "emitted": 63, - "rejected": [] - }, - "log_time": { - "mode": "create", - "main_table": "time_entries", - "locations": { - "activity": "time_entries.activity_id -> enumerations.name (exact)", - "comments": "time_entries.comments (exact)", - "hours": "time_entries.hours (number)", - "issue_subject": "time_entries.issue_id -> issues.subject (exact)", - "project": "time_entries.project_id -> projects.name (exact)", - "spent_on": "time_entries.spent_on (date)" - }, - "placed_by_name": [], - "versions": null, - "placed_from_another_kind": [], - "demonstrations": 2, - "children": {}, - "dispatch_children": {}, - "lists": {}, - "side_rows": [], - "logs": [ - "journal_details" - ], - "log_candidates": [], - "touched_rows": [ - { - "table": "user_preferences", - "selector": { - "id": { - "literal": "1" - } - }, - "columns": { - "others": "other" - }, - "row": "user_preferences row id=1", - "by_key": true - } - ], - "unexcused": {}, - "refused": [], - "unresolved": {}, - "emitted": 4, - "rejected": [] - }, - "ensure_issue": { - "mode": "create", - "main_table": "issues", - "locations": { - "assignee": "issues.assigned_to_id -> users.firstname || ' ' || lastname (exact)", - "priority": "issues.priority_id -> enumerations.name (exact)", - "project": "issues.project_id -> projects.name (exact)", - "subject": "issues.subject (exact)", - "tracker": "issues.tracker_id -> trackers.name (exact)" - }, - "placed_by_name": [], - "versions": null, - "placed_from_another_kind": [], - "demonstrations": 2, - "children": {}, - "dispatch_children": {}, - "lists": {}, - "side_rows": [ - { - "table": "custom_values", - "count": 2, - "unless": null - } - ], - "logs": [ - "journal_details" - ], - "log_candidates": [], - "touched_rows": [ - { - "table": "user_preferences", - "selector": { - "id": { - "literal": "1" - } - }, - "columns": { - "others": "other" - }, - "row": "user_preferences row id=1", - "by_key": true - } - ], - "unexcused": {}, - "refused": [], - "unresolved": {}, - "emitted": 2, - "rejected": [] - } - } -} \ No newline at end of file diff --git a/packages/flow/benchmark/generate_contracts/summarize.py b/packages/flow/benchmark/generate_contracts/summarize.py deleted file mode 100644 index a277508deb..0000000000 --- a/packages/flow/benchmark/generate_contracts/summarize.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Count a validation run: correct writes confirmed, faults stopped, by kind. - - python benchmark/generate_contracts/summarize.py APP WORKDIR - -A case passes when the verifier says CONFIRMED. The oracle's label says what -the write really did: ``correct`` (or no write needed), or harm. A correct -write that is not CONFIRMED is a false stop; a harm that is CONFIRMED is a -miss. Fault scope (in or out of what the verifier claims) comes from the -pack's ``faults.yaml``. -""" - -from __future__ import annotations - -import json -import sys -from collections import defaultdict -from pathlib import Path - -import yaml -from demo_lib import FCG - -GOOD = { - "correct" -} # "nothing" (no write where one was needed) and "harm" must be stopped - - -def main() -> None: - app, work = sys.argv[1], Path(sys.argv[2]) - scope = { - f["id"]: f.get("scope", "in") - for f in yaml.safe_load( - (FCG / "apps" / app / "faults" / "faults.yaml").read_text() - ) - } - rows = [ - json.loads(x) - for x in (work / "validate.jsonl").read_text().splitlines() - if x.strip() - ] - by_kind: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int)) - misses, false_stops = [], [] - for r in rows: - k = r["kind"] - # A recheck that refutes a confirmed write turns it into a stop (late). - ok = r["verdict"]["outcome"] == "CONFIRMED" and r.get("recheck", {}).get("outcome", "CONFIRMED") == "CONFIRMED" - label = str(r["oracle"].get("label")) - harm = label not in GOOD - sc = scope.get(r["case"].split("-", 1)[1], "in") - if harm: - by_kind[k][f"harm_{sc}"] += 1 - if ok: - by_kind[k][f"harm_{sc}_confirmed"] += 1 - misses.append((r["case"], sc, label)) - else: - by_kind[k]["correct"] += 1 - if ok: - by_kind[k]["correct_confirmed"] += 1 - else: - false_stops.append( - ( - r["case"], - r["verdict"]["outcome"], - "; ".join(r["verdict"]["reasons"])[:160], - ) - ) - print( - "| Kind | Correct writes confirmed | In-scope harms stopped | Out-of-scope harms stopped |" - ) - print("| --- | --- | --- | --- |") - for k, c in sorted(by_kind.items()): - hi, ho = c["harm_in"], c["harm_out"] - print(f"| `{k}` | {c['correct_confirmed']}/{c['correct']} | {hi - c['harm_in_confirmed']}/{hi} | " - f"{ho - c['harm_out_confirmed']}/{ho} |") # fmt: skip - print() - for m in misses: - print("MISS", *m) - for f in false_stops: - print("FALSE STOP", *f) - - -if __name__ == "__main__": - main() diff --git a/packages/flow/benchmark/generate_contracts/validate.py b/packages/flow/benchmark/generate_contracts/validate.py deleted file mode 100644 index 6e2b4f631d..0000000000 --- a/packages/flow/benchmark/generate_contracts/validate.py +++ /dev/null @@ -1,158 +0,0 @@ -"""Judge generated contracts on a first-study app: correct writes and faults. - - FCG_INSTANCE=1 python benchmark/generate_contracts/validate.py dolibarr WORKDIR [--only T01 F05] - -For every task of the study, the pack's reference correct write; for every -planted fault, the fault. Each case: reset the app, pick the contract with -``verify_contract.choose`` (from the task's values only), ``begin_write``, -write, ``verify_write``, and the study's oracle labels what happened. Results -append to ``WORKDIR/validate.jsonl``; a case already there is skipped. - -The tasks, faults and oracle are read here only, never by the generator. -""" - -from __future__ import annotations - -import argparse -import json -import os -import tempfile -import time -import traceback -from pathlib import Path -from typing import Any - -import yaml -from demo_lib import FCG, fixture, load - -from openadapt_flow.generate import verify_contract as gen -from openadapt_flow.verifier import Actor, begin_write, recheck, verify_write - -LATE_WAIT_S = 150 - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("app") - ap.add_argument("work", type=Path) - ap.add_argument("--only", nargs="*") - ap.add_argument( - "--actor-id", required=True, help="the bot's id as the contracts bind it" - ) - ap.add_argument("--actor-param", default="bot_user_id") - ap.add_argument("--no-faults", action="store_true") - ap.add_argument("--no-correct", action="store_true") - args = ap.parse_args() - os.environ.setdefault("OPENADAPT_LOCAL_SEAL", "off") - app_dir = FCG / "apps" / args.app - fx = fixture(args.app) - oracle = load(f"fcg_oracle_{args.app}", app_dir / "oracle.py") - faults = load(f"fcg_faults_{args.app}", app_dir / "faults" / "faults.py") - tasks = {t["id"]: t for t in yaml.safe_load((app_dir / "tasks.yaml").read_text())} - manifest = args.work / "out" / "manifest.yaml" - out = args.work / "validate.jsonl" - done = set() - if out.exists(): - done = { - json.loads(x)["case"] for x in out.read_text().splitlines() if x.strip() - } - cases: list[tuple[str, dict, Any, float]] = [] - if not args.no_correct: - for tid, task in tasks.items(): - cases.append( - ( - f"correct-{tid}", - task, - lambda t=task: faults.correct_write(t, fx.bot()), - 0.0, - ) - ) - if not args.no_faults: - for f in yaml.safe_load((app_dir / "faults" / "faults.yaml").read_text()): - task = tasks[f["task"]] - fn = getattr(faults, f["id"]) - late = 0.0 - if f.get("class") == "late_write" or f.get("after_snapshot_delay_s"): - late = max(LATE_WAIT_S, float(f.get("after_snapshot_delay_s") or 0)) - cases.append( - (f"fault-{f['id']}", task, - lambda fn=fn, t=task: fn(t, fx.bot(), fx.other_user(), fx.read_access()), late) - ) # fmt: skip - for case, task, act, late in cases: - short = case.split("-", 1)[1] - if case in done or ( - args.only and short not in args.only and task["id"] not in args.only - ): - continue - while os.getloadavg()[0] >= 35: - time.sleep(30) - rec: dict[str, Any] = {"case": case, "task": task["id"], "kind": task["kind"]} - fx.reset() - before = oracle.snapshot() - pending = None - try: - contract, params = gen.choose( - manifest, - task["kind"], - task["values"], - {args.actor_param: args.actor_id}, - ) - rec["contract"] = contract.name - tmp = tempfile.mkdtemp() - pending = begin_write(contract, params, actor=Actor("agent", "generated-check"), - ledger=None, write_key=f"{case}-{time.time_ns()}", workdir=tmp) # fmt: skip - except Exception as exc: # noqa: BLE001 - a stop before the write - rec["verdict"] = { - "outcome": "BLOCKED_AT_BEGIN", - "reasons": [f"{type(exc).__name__}: {exc}"], - } - try: - rec["claim"] = act() - except Exception as exc: # noqa: BLE001 - rec["claim"] = { - "status": "not_done", - "note": f"{type(exc).__name__}: {exc}", - } - if pending is not None: - try: - r = verify_write(pending) - rec["verdict"] = { - "outcome": getattr(r.outcome, "value", str(r.outcome)), - "reasons": list(r.reasons), - "recheck_due_at": getattr(r, "recheck_due_at", None), - } - except Exception as exc: # noqa: BLE001 - rec["verdict"] = {"outcome": "VERIFIER_CRASH", "reasons": [f"{type(exc).__name__}: {exc}"], - "trace": traceback.format_exc()[-2000:]} # fmt: skip - if late: - time.sleep(late) - if hasattr(fx, "wait_late_writes"): - fx.wait_late_writes() - if rec["verdict"]["outcome"] == "CONFIRMED" and rec["verdict"].get("recheck_due_at"): - # A write that lands after the check: the recheck the contract - # declares (frame.database.recheck_after_s) reads it. - try: - late_r = recheck(contract, r, params=params, - actor=Actor("agent", "generated-check"), wait=True) # fmt: skip - rec["recheck"] = { - "outcome": getattr(late_r.outcome, "value", str(late_r.outcome)), - "reasons": list(late_r.reasons), - } - except Exception as exc: # noqa: BLE001 - rec["recheck"] = {"outcome": "RECHECK_CRASH", "reasons": [f"{type(exc).__name__}: {exc}"]} - after = oracle.snapshot() - try: - rec["oracle"] = oracle.judge(task, before, after) - except Exception as exc: # noqa: BLE001 - rec["oracle"] = {"label": "oracle_error", "reasons": [str(exc)]} - with out.open("a", encoding="utf-8") as f: - f.write(json.dumps(rec, default=str, ensure_ascii=False) + "\n") - v = rec["verdict"] - print(f"{case} [{task['kind']}]: {v['outcome']} | oracle {rec['oracle'].get('label')} | " - f"{'; '.join(v.get('reasons', []))[:200]}", flush=True) # fmt: skip - fx.reset() - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) From cd4d481762e3dcac1fed2f8d9b46fc1f0fd4a93c Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 23:05:51 -0400 Subject: [PATCH 87/94] CAPABILITIES, artifact inventory and review brief without the moved evidence --- CAPABILITIES.md | 2 +- llms-full.txt | 2 +- packages/flow/public-artifacts.json | 31 ----------------------------- 3 files changed, 2 insertions(+), 33 deletions(-) diff --git a/CAPABILITIES.md b/CAPABILITIES.md index 6c9b90f141..78a8264316 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,7 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 4, Redmine 2, EspoCRM 3, against 11 hand decisions each; every in-scope planted fault stopped (22/22, 19/19, 21/21); correct writes confirmed 40/43, 32/38, 27/40; see [RESULTS.md](packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 4, Redmine 2, EspoCRM 3, against 11 hand decisions each; every in-scope planted fault stopped (22/22, 19/19, 21/21); correct writes confirmed 40/43, 32/38, 27/40; evidence in the private study repository) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/llms-full.txt b/llms-full.txt index 6a37e4042a..97a6ef51df 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -544,7 +544,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 4, Redmine 2, EspoCRM 3, against 11 hand decisions each; every in-scope planted fault stopped (22/22, 19/19, 21/21); correct writes confirmed 40/43, 32/38, 27/40; see [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/generate_contracts/RESULTS.md)) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 4, Redmine 2, EspoCRM 3, against 11 hand decisions each; every in-scope planted fault stopped (22/22, 19/19, 21/21); correct writes confirmed 40/43, 32/38, 27/40; evidence in the private study repository) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/packages/flow/public-artifacts.json b/packages/flow/public-artifacts.json index 78f8e1a024..023180f5a1 100644 --- a/packages/flow/public-artifacts.json +++ b/packages/flow/public-artifacts.json @@ -272,37 +272,6 @@ "benchmark/frappe_lending/environment.lock.json", "benchmark/gate_per_class/results/after.json", "benchmark/gate_per_class/results/before.json", - "benchmark/generate_contracts/results/dolibarr/answers.yaml", - "benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.create.yaml", - "benchmark/generate_contracts/results/dolibarr/contracts/create_customer/address+country+email+name+phone+town+zip.8386ba67.found.yaml", - "benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.create.yaml", - "benchmark/generate_contracts/results/dolibarr/contracts/create_draft_invoice/customer+invoice_date+town+linesx2.17de9df9.found.yaml", - "benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.create.yaml", - "benchmark/generate_contracts/results/dolibarr/contracts/create_product/label+price_excl_vat+ref+vat_rate.bcd72fd0.found.yaml", - "benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.found.yaml", - "benchmark/generate_contracts/results/dolibarr/contracts/update_customer/customer+new_email+new_phone+town.110f7554.update.yaml", - "benchmark/generate_contracts/results/dolibarr/manifest.yaml", - "benchmark/generate_contracts/results/dolibarr/report.json", - "benchmark/generate_contracts/results/espocrm/answers.yaml", - "benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.create.yaml", - "benchmark/generate_contracts/results/espocrm/contracts/create_contact/account+account_city+email+first_name+last_name+phone+title.fef035cb.found.yaml", - "benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.create.yaml", - "benchmark/generate_contracts/results/espocrm/contracts/create_opportunity/account+account_city+amount+close_date+currency+name+stage.1d2f2376.found.yaml", - "benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.create.yaml", - "benchmark/generate_contracts/results/espocrm/contracts/log_call/account+account_city+contact+date_start+description+direction+duration_minutes+s.dca67171.found.yaml", - "benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.found.yaml", - "benchmark/generate_contracts/results/espocrm/contracts/update_lead/email+company+field+first_name+last_name+value.a43b40a4.update.yaml", - "benchmark/generate_contracts/results/espocrm/manifest.yaml", - "benchmark/generate_contracts/results/espocrm/report.json", - "benchmark/generate_contracts/results/redmine/answers.yaml", - "benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.create.yaml", - "benchmark/generate_contracts/results/redmine/contracts/create_issue/assignee+customer_ref+description+environment+priority+project+subject+tracker.d268453f.found.yaml", - "benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.create.yaml", - "benchmark/generate_contracts/results/redmine/contracts/log_time/activity+comments+hours+issue_subject+project+spent_on.a2ffb74d.found.yaml", - "benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.found.yaml", - "benchmark/generate_contracts/results/redmine/contracts/update_issue/notes+project+status+subject.4aa4d777.update.yaml", - "benchmark/generate_contracts/results/redmine/manifest.yaml", - "benchmark/generate_contracts/results/redmine/report.json", "benchmark/governed_run/results.json", "benchmark/grounding_eval/results.json", "benchmark/grounding_eval/results_sweep.json", From 99db906c4b8a525561f762a7bd8831a6ebd2f3e3 Mon Sep 17 00:00:00 2001 From: abrichr Date: Fri, 30 Oct 2026 23:51:43 -0400 Subject: [PATCH 88/94] Round 2 review: versions targets, per-surface fields, strict answers, PostgreSQL privileges - verify_run: a target is new only when the read before the write shows it wasn't there (the effect's own surface read for a surfaces verifier); with no read it is treated as existing, so no baseline version stops it as missing. frame.versions.target_surface limits targets to the main record's surface. - Generator: a version column first, else the audit trail; the write-event filter compares update rows (one code across every update kind) with create kinds' rows only, and keeps both codes. - frame.target_fields_by_surface: each declared row may change only its own table's fields. - A key back to the main table must name exactly one row too. - Bookkeeping answers: known keys only, accept: true to take the proposal, logs_add, columns_add and unchecked only from what the question listed (logs per kind); unnamed open columns are pinned empty or refuse the kind. - PostgreSQL check_read_only: effective table, sequence, schema and database privileges, including inherited roles. --- packages/flow/docs/GENERATE_CONTRACTS.md | 51 ++- packages/flow/docs/VERIFY_RUN.md | 9 +- .../generate/verify_contract.py | 375 ++++++++++++---- .../openadapt_flow/runtime/effects/frame.py | 29 +- .../runtime/effects/versions.py | 14 +- packages/flow/openadapt_flow/verify_run.py | 40 +- .../tests/test_generate_verify_contract.py | 26 +- ...est_generate_verify_contract_attacks_r2.py | 423 ++++++++++++++++++ 8 files changed, 842 insertions(+), 125 deletions(-) create mode 100644 packages/flow/tests/test_generate_verify_contract_attacks_r2.py diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 931bd9d147..d7e6f31abe 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -195,8 +195,11 @@ row a key points to (an invoice line's price from its product). A key, a link to a row the write added, the year, month, week or day of a date the task gives, and a generated token (a UUID) are recognized as such. Columns that differ between demonstrations, free text every demonstration left -empty, and the write's time stamps are not checked; the bookkeeping question -lists each of them by table, so a person sees every column left open. +empty, and the write's time stamps are listed in the bookkeeping question, +and each is left unchecked only if the answer names it (`unchecked`). Free +text the answer doesn't name must stay empty; a kind with any other column +the answer doesn't name gets no contract. The proposed answer names every +listed column, so accepting it is explicit. **Rows the app adds on its own.** A row type the app added the same number of times in every demonstration (an agenda event, a stream note, a price-history @@ -205,9 +208,10 @@ added rows differ in number between demonstrations is listed in the bookkeeping question and is a log only if the person names it (`logs_add`); otherwise every added row stops the write. The person may also name a counted table (an access log the app writes once per request: the agent may -make more requests than the demonstration did). A named table is a log for -every kind: rows may be added to it, and existing rows must not change. The -generator never decides a table is a log from its name. +make more requests than the demonstration did). A named table is a log only +for the kinds whose demonstrations added rows there: rows may be added to +it, and existing rows must not change. The generator never decides a table +is a log from its name. **Changes the app makes to existing rows.** A column of an existing row that the demonstrations changed with no task value is excused only on that row: @@ -218,6 +222,15 @@ rows or tables. Time stamps set during the write, counters that went up and columns set to the acting user are proposed; anything else is listed and excused only if the person adds it (`columns_add`). A changed row the generator can't identify is listed too, and a write that changes it stops. +Each declared row may change only its own table's excused columns +(`frame.target_fields_by_surface`): a parent row the app touches can't take +the target's changed fields, and the target can't take the parent's. + +**Answers.** `bookkeeping: accept` takes the proposal. A mapping may have +`accept: true` (take the proposed columns), `logs_add`, `columns_add`, +`columns_remove` and `unchecked`, and each can name only what the question +listed. Any other key, or a table or column the question didn't list, is +refused. A mapping without `accept: true` takes only what it names. **The frame.** Every contract uses the whole-database frame (`frame.database`, with the store's dialect): a row added, removed or changed @@ -230,17 +243,21 @@ fails and no contract is written. **Lost updates.** An update's contracts set `frame.versions`, so another user's save to the target inside the window stops the write. The generator -takes the versions from the app's own audit trail when every update +first takes a whole-number column named as a version that went up by one in +every demonstration (Redmine's `lock_version`): every existing record has a +version there. Otherwise it takes the app's own audit trail when every update demonstration added a row there that names the record and the acting user -(Redmine's journals, EspoCRM's `action_history_record`, Dolibarr's agenda -events). The read is narrowed to the record's type and to write events: a +(EspoCRM's `action_history_record`, Dolibarr's agenda events). The read is narrowed to the record's type and to write events: a text column, not an id, that holds one code of letters and underscores in -every update row and in none of the rows creates added to the same table -(EspoCRM's `action = update`, Dolibarr's `code = AC_COMPANY_MODIFY`), so -another user only viewing the record doesn't count. A time stamp or a token -the demonstrations happened to share is never used. Otherwise it uses a -whole-number column named as a version that went up by one in every -demonstration (Redmine's `lock_version`). With neither, the update has no +every row any update kind added (so it doesn't vary with the fields saved) +and only other codes in the rows create kinds added (EspoCRM's `action`). +The read keeps the update code and the create codes, so a record the app +created has a first version, and another user only viewing the record +doesn't count. A time stamp or a token the demonstrations happened to share +is never used. Only the main record is a target (`target_surface`). A target +the read before the write doesn't show as new is treated as existing, and +with no version before the write it stops as `missing` (a record saved +before the trail began, or outside the app). With neither, the update has no version check, and `report.json` shows `versions: null`. If the app stops writing the trail (an agenda setting turned off), the agent's own save has no new version, and the verifier stops the write rather than pass it. @@ -255,6 +272,12 @@ The generator never leaves a step for a person to apply later. A contract that needs something the store doesn't have is either set up, with a flag and an admin connection you give, or not written, with the reason. +The read account must be read-only. On PostgreSQL the check uses its +effective privileges, so a role it is a member of counts: `INSERT`, +`UPDATE`, `DELETE`, `TRUNCATE`, `TRIGGER` or `REFERENCES` on any table, +`USAGE` or `UPDATE` on a sequence, or `CREATE` on a schema or the database +stops the generator. + - `--grant-trigger --admin-read admin.yaml --reader-account "'oa_reader'@'%'"`: grants `TRIGGER` on the database to the read account (MySQL, MariaDB). The account name is split into user and host and escaped by the driver. diff --git a/packages/flow/docs/VERIFY_RUN.md b/packages/flow/docs/VERIFY_RUN.md index 9b4230ca07..85d5225c53 100644 --- a/packages/flow/docs/VERIFY_RUN.md +++ b/packages/flow/docs/VERIFY_RUN.md @@ -99,6 +99,9 @@ frame: identity_field: pid # default: id, else the whole record ignore_fields: [date] # fields that change on their own target_fields: [phone_home] # default: the fields the effects name + # target_fields_by_surface: # optional: per surface, replaces target_fields + # record: [phone_home] # a declared row of `record` may change these + # raw_account: [updated_at] # a table counts as the surface `views` maps it to attribution: # optional kind: audit_read # or record_field, with server_set: [...] actor_field: user @@ -411,6 +414,7 @@ frame: FROM hfj_res_ver v JOIN hfj_resource r ON r.res_id = v.res_id record_field: id # the version row's record target_field: id # the target's field in the read set (default: identity_field) + # target_surface: record # only records matched on this surface are targets version_field: ver # author_field: user_id # when the app records who saved each version # self: [{param: bot_user_id}] @@ -480,7 +484,10 @@ Anything else stops the run, never a pass: A record created in the window has no earlier version to overwrite and isn't judged. Whether a target existed is told by `frame.identity_field` (default `id`), never by `target_field`'s value after the write: a target that existed -with no version under its current `target_field` value is `missing`. Only targets are judged: another user's save to an unrelated record +with no version under its current `target_field` value is `missing`. A target +is new only when the read before the write shows it wasn't there: the +effect's own surface read for a `surfaces` verifier, else the verifier's +records. With no such read, or no identity, it is treated as existing. Only targets are judged: another user's save to an unrelated record leaves this check as it was (the frame and its attribution judge that record, as before). The versions are read before the records at the baseline, so a save between the two reads shows in the record baseline. The diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 3b01fa85cd..12afb98125 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -941,9 +941,6 @@ def _sides( plan.anchor_extra[col] = _change_kind( schema, plan.anchor, col, plan.demos, plan.anchors, actor_values ) - plan.versions = _audit_source( - schema, plan, actor_values, all_demos - ) or _version_column(schema, plan) _WHOLE_NUMBER = re.compile(r"^-?[0-9]+$") @@ -1122,7 +1119,8 @@ def _audit_source( schema: Schema, plan: KindPlan, actor_values: Mapping[str, str], - all_demos: Sequence[Demo] = (), + create_demos: Sequence[Demo] = (), + update_demos: Sequence[Demo] = (), ) -> Optional[dict[str, Any]]: """A table that gained, in every update demonstration, a row naming the main record (a column holding its key) and the acting user (a column @@ -1170,7 +1168,7 @@ def _audit_source( # narrows the read. Anything else the demonstrations had in common # (the action, the client address) could hide another user's save, # so it isn't used: an entry by anyone else stops the write. - where = {} + where: dict[str, Any] = {} names = {plan.anchor.lower(), plan.anchor.lower().split("_")[-1]} for col in table.columns: if col.name in (lc, ac) or col.kind != "text": @@ -1180,35 +1178,41 @@ def _audit_source( v = next(iter(values)) if v is not None and v.lower() in names and v.isidentifier(): where[col.name] = v - # Write events only: a column whose value in these rows (every - # update) differs from its value in the rows creates added to the - # same table (EspoCRM's ``action``: update, not create or read; - # Dolibarr's ``code``: COMPANY_MODIFY). + # Write events only (another user only viewing the record doesn't + # count): a code column (letters and underscores, not an id) that + # holds one value in every row any update kind's demonstrations + # added here, so it doesn't vary with the fields saved, and only + # other codes in the rows create kinds added. The read keeps those + # codes, the update's and the creates' (a record the app created has + # its first version then). A time stamp, a token or a text the + # demonstrations happened to share is never used. + updated = [ + ch.after or {} for d in update_demos for ch in d.diff.of(name, "added") + ] created = [ - ch.after or {} - for d in all_demos - if d not in plan.demos - for ch in d.diff.of(name, "added") + ch.after or {} for d in create_demos for ch in d.diff.of(name, "added") ] for col in table.columns: - if col.name in (lc, ac) or col.name in table.primary_key: + if ( + col.name in (lc, ac) + or col.name in table.primary_key + or col.kind != "text" + or col.name.lower().endswith("id") + or col.name in where + ): continue - mine = {r.get(col.name) for r in rows} + mine = {r.get(col.name) for r in updated} theirs = {r.get(col.name) for r in created} - # One event code in every update, and in none of the creates' - # rows (COMPANY_MODIFY against COMPANY_CREATE, PRODUCT_CREATE). - # Only a code (letters and underscores, not an id column): a time - # stamp, a token or a text the demonstrations happened to share - # would hide other users' saves. - if len(mine) == 1 and created and mine.isdisjoint(theirs): - v = next(iter(mine)) - if ( - v is not None - and col.kind == "text" - and not col.name.lower().endswith("id") - and re.fullmatch(r"[A-Za-z_]{1,64}", v) - ): - where[col.name] = v + codes = mine | theirs + if ( + len(mine) == 1 + and theirs + and not (mine & theirs) + and all( + x is not None and re.fullmatch(r"[A-Za-z_]{1,64}", x) for x in codes + ) + ): + where[col.name] = sorted(x for x in codes if x is not None) found = { "kind": "audit", "table": name, @@ -1238,12 +1242,15 @@ def _versions_frame( "read": {"kind": "sql", **settings.read, "sql_query": sql}, "record_field": "id", "target_field": "id", + "target_surface": "record", "version_field": "ver", } table = v["table"] vpk = b.schema.tables[table].primary_key[0] conds = " AND ".join( f"{b.text('t', table, c)} = {b.literal(val)}" + if isinstance(val, str) + else f"{b.text('t', table, c)} IN ({', '.join(b.literal(x) for x in val)})" for c, val in sorted(v["where"].items()) ) sql = ( @@ -1256,6 +1263,7 @@ def _versions_frame( "read": {"kind": "sql", **settings.read, "sql_query": sql}, "record_field": "id", "target_field": "id", + "target_surface": "record", "version_field": "ver", "author_field": "author", "self": [{"param": v["actor_param"]}], @@ -1450,7 +1458,11 @@ def path(self, alias: str, table: str, loc: Location, depth: int = 0) -> str: sub = f"a{depth + 1}" inner = self.path(sub, hop.table, rest, depth + 1) live = self.live(sub, hop.table) - if not rest.hops and hop.table != self.anchor: + # A child row's link to its own record names the target by identity + # and needs no check; any other key into the main table (a parent + # issue named by its subject) does. + child_link = hop.table == self.anchor and table != self.anchor + if not rest.hops and not child_link: # The value must name exactly one live row of its table: a # second "Jon Park", or "Mary Ann|Lee" beside "Mary|Ann Lee", # makes it NULL, so nothing matches and the write stops. @@ -1650,6 +1662,14 @@ class Settings: #: Tables a view replaces in surface reads (the SQL guard refuses the #: table's name): table -> view name. views: dict[str, str] = field(default_factory=dict) + #: Table -> columns of a new row the person named as not checked (they + #: vary, are free text left empty, or are the write's time stamp). + unchecked: dict[str, set[str]] = field(default_factory=dict) + + +#: Roles of a new row's column that no rule checks: named by the person +#: (``bookkeeping.unchecked``), else pinned empty (free) or the kind refused. +OPEN_ROLES = ("varies", "free", "timestamp") def _value_expr(param: str, rule: str) -> dict[str, Any]: @@ -1692,6 +1712,8 @@ def build_contract( surfaces: dict[str, Surface] = {} effects: list[dict[str, Any]] = [] target_fields: set[str] = set() + #: Surface -> the fields its declared rows may change. + by_surface: dict[str, set[str]] = {} named_tables: set[str] = {anchor} # The main record, with every column and the values its parameters reach. @@ -1729,7 +1751,16 @@ def build_contract( for col, role in sorted(roles.items()): if flavor == "found" and role.role in ("actor", "copied"): continue - _pin(b, main, anchor, col, role, match, settings.actor_params) + _pin( + b, + main, + anchor, + col, + role, + match, + settings.actor_params, + settings.unchecked, + ) if plan.mode == "create" and flavor == "create": support = _support(plan, variant) for p in plan.optional: @@ -1837,7 +1868,16 @@ def build_contract( for col, role in sorted(g.roles.items()): if role.role == "link" or (found and role.role in ("actor", "copied")): continue - _pin(b, surf, g.table, col, role, m, settings.actor_params) + _pin( + b, + surf, + g.table, + col, + role, + m, + settings.actor_params, + settings.unchecked, + ) effect = { "kind": "record_written", "probe": f"surface={sname}|a row beside the main record holds {p}", @@ -1866,12 +1906,11 @@ def build_contract( "expected_count": g.after_count, } ) - if plan.mode == "update": - target_fields.update(g.changed_columns) + if plan.mode == "update" and not found: + by_surface.setdefault(sname, set()).update(g.changed_columns) # Existing rows the app changes beside the write (a counter row, the # account a contact belongs to): declared by key or by the task's values, # with only the confirmed columns allowed to move, on those rows only. - touched_fields: set[str] = set() for spec in plan.touched: # A row changed by key (a user's preferences, which even a read may # touch) is excused in ``found`` too; a row named by the task's @@ -1902,7 +1941,11 @@ def build_contract( "expected_count": 1, } ) - touched_fields.update(cols) + # Only on this table's rows: a parent row the app touches can't take + # the target's fields, nor the target a parent's. + by_surface.setdefault(name, set()).update(cols) + if spec["table"] == anchor: + by_surface.setdefault("record", set()).update(cols) # A new row a link row names (a new email address). for ref in plan.added_refs.values(): p = ref["param"] @@ -1954,9 +1997,13 @@ def build_contract( # Main and child effects read named surfaces: no other read is judged # against them, and the whole-database frame covers every other table. frame: dict[str, Any] = {} - frame["target_fields"] = sorted( - touched_fields | (set() if flavor == "found" else target_fields) - ) + if not found: + by_surface.setdefault("record", set()).update(target_fields) + # Per surface: each declared row may change only its own table's fields. + frame["target_fields"] = sorted(set().union(*by_surface.values())) + frame["target_fields_by_surface"] = { + k: sorted(v) for k, v in sorted(by_surface.items()) if v + } if plan.mode == "update": # Another user's save to the target inside the window (a lost # update) shows only in the target's versions. @@ -2048,7 +2095,14 @@ def _pin( role: ColumnRole, match: dict[str, Any], actor_params: Mapping[str, str], + unchecked: Mapping[str, set[str]] = {}, ) -> None: + if role.role == "free" and col not in unchecked.get(table, set()): + # Empty in every demonstration and not named as unchecked: it stays + # empty. + alias = _add_column(surf, f"empty__{_alias(col)}", b.empty("t", table, col)) + match[alias] = {"literal": "1"} + return if role.role == "constant": column = b.schema.tables[table].column(col) if role.value is not None and column is not None and column.kind == "text": @@ -2145,15 +2199,13 @@ def _raw_effect( or role.role != "constant" or (role.detail or {}).get("is_default") ): - if role is None or role.role in ( - "key", - "timestamp", - "link", - "varies", - "copied", - "actor", - "free", - ): + if role is not None and role.role in OPEN_ROLES: + # Set freely only when the person named it unchecked; else + # it must hold its default. + if c.name in settings.unchecked.get(table, set()): + nrf.add(c.name) + continue + if role is None or role.role in ("key", "link", "copied", "actor"): nrf.add(c.name) continue nrf.add(c.name) @@ -2460,13 +2512,53 @@ def check_read_only(connect: Any, dialect: str) -> list[str]: problems.append( "the read account is a superuser or can create roles or databases" ) + # Effective privileges, so a role the account is a member of (or a + # predefined role such as pg_write_all_data) counts too, on every + # table, view and sequence of every schema the app can use. + user_schemas = ( + "n.nspname NOT IN ('pg_catalog', 'information_schema') " + "AND n.nspname NOT LIKE 'pg\\_toast%' " + "AND n.nspname NOT LIKE 'pg\\_temp%'" + ) + cur.execute( + "SELECT n.nspname, c.relname, p.priv FROM pg_class c " + "JOIN pg_namespace n ON n.oid = c.relnamespace " + "CROSS JOIN unnest(ARRAY['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', " + "'TRIGGER', 'REFERENCES']) AS p(priv) " + "WHERE c.relkind IN ('r', 'p', 'v', 'm', 'f') AND " + + user_schemas + + " AND has_table_privilege(c.oid, p.priv) " + "ORDER BY 1, 2, 3 LIMIT 20" + ) + for schema_name, t, priv in cur.fetchall(): + problems.append( + f"the read account has {priv} on table {schema_name}.{t}" + ) + cur.execute( + "SELECT n.nspname, c.relname, p.priv FROM pg_class c " + "JOIN pg_namespace n ON n.oid = c.relnamespace " + "CROSS JOIN unnest(ARRAY['USAGE', 'UPDATE']) AS p(priv) " + "WHERE c.relkind = 'S' AND " + + user_schemas + + " AND has_sequence_privilege(c.oid, p.priv) " + "ORDER BY 1, 2, 3 LIMIT 20" + ) + for schema_name, t, priv in cur.fetchall(): + problems.append( + f"the read account has {priv} on sequence {schema_name}.{t} " + "(it can move the counter)" + ) cur.execute( - "SELECT table_name FROM information_schema.role_table_grants " - "WHERE grantee = current_user AND privilege_type IN " - "('INSERT','UPDATE','DELETE','TRUNCATE') AND table_schema = current_schema() LIMIT 5" + "SELECT n.nspname FROM pg_namespace n WHERE " + + user_schemas + + " AND has_schema_privilege(n.oid, 'CREATE') ORDER BY 1" ) - for (t,) in cur.fetchall(): - problems.append(f"the read account can write table {t}") + for (schema_name,) in cur.fetchall(): + problems.append(f"the read account has CREATE on schema {schema_name}") + cur.execute("SELECT has_database_privilege(current_database(), 'CREATE')") + row = cur.fetchone() + if row and row[0]: + problems.append("the read account has CREATE on the database") finally: conn.rollback() conn.close() @@ -2675,6 +2767,22 @@ def propose( if len(env) < 8: env = (env + "-environment")[:64] plans = [analyze(schema, k, demos, actor_params) for k in kinds] + # Versions of an update's target, chosen once every kind's mode is + # known: the creates' and the updates' audit rows tell write events apart. + create_kinds = {p.kind for p in plans if p.mode == "create"} + update_kinds = {p.kind for p in plans if p.mode == "update"} + for p in plans: + if p.mode == "update" and not p.refused: + # A version column first: every existing row has a version + # there. An audit trail misses records saved before it, or + # outside the app, and such a target stops as `missing`. + p.versions = _version_column(schema, p) or _audit_source( + schema, + p, + actor_params, + [d for d in demos if d.kind in create_kinds], + [d for d in demos if d.kind in update_kinds], + ) questions: list[Question] = [] _share_locations(schema, plans, kinds) @@ -2769,11 +2877,44 @@ def propose( # Q: bookkeeping, for the whole app at once. ans = _answer(answers, "bookkeeping") - accepted = ans == "accept" or isinstance(ans, dict) + if ans is not None and ans != "accept" and not isinstance(ans, dict): + raise GenerateError( + "bookkeeping: answer `accept` or a mapping with accept, logs_add, " + "columns_add, columns_remove and unchecked" + ) + if isinstance(ans, dict): + unknown = set(ans) - { + "accept", + "logs_add", + "columns_add", + "columns_remove", + "unchecked", + } + if unknown: + raise GenerateError(f"bookkeeping: unknown answer keys {sorted(unknown)}") + if "accept" in ans and not isinstance(ans["accept"], bool): + raise GenerateError("bookkeeping.accept must be true or false") + # Only `accept` (or `accept: true`) takes the proposed columns; a mapping + # without it takes only what it names. + accepted = ans == "accept" or (isinstance(ans, dict) and ans.get("accept") is True) ans = ans if isinstance(ans, dict) else {} - log_add = set(ans.get("logs_add", [])) - columns_add = {t: set(c) for t, c in dict(ans.get("columns_add", {})).items()} - columns_remove = {t: set(c) for t, c in dict(ans.get("columns_remove", {})).items()} + + def table_cols(key: str) -> dict[str, set[str]]: + raw = ans.get(key) or {} + if not isinstance(raw, dict) or not all( + isinstance(c, list) and all(isinstance(x, str) for x in c) + for c in raw.values() + ): + raise GenerateError(f"bookkeeping.{key}: a mapping of table -> [columns]") + return {str(t): set(c) for t, c in raw.items()} + + raw_logs = ans.get("logs_add") or [] + if not isinstance(raw_logs, list) or not all(isinstance(x, str) for x in raw_logs): + raise GenerateError("bookkeeping.logs_add: a list of tables") + log_add = set(raw_logs) + columns_add = table_cols("columns_add") + columns_remove = table_cols("columns_remove") + unchecked_named = table_cols("unchecked") proposed: dict[str, set[str]] = {} offered: dict[str, set[str]] = {} where_seen: dict[str, set[str]] = {} @@ -2789,36 +2930,77 @@ def propose( row_spec["table"], set() ).add(col) where_seen.setdefault(row_spec["table"], set()).add(row_spec["row"]) - excused: dict[str, set[str]] = {} - if accepted: - for t in set(proposed) | set(columns_add): - excused[t] = (proposed.get(t, set()) | columns_add.get(t, set())) - ( - columns_remove.get(t, set()) + for t, cols in columns_add.items(): + extra = cols - proposed.get(t, set()) - offered.get(t, set()) + if extra: + raise GenerateError( + f"bookkeeping.columns_add names {t}: {', '.join(sorted(extra))}, " + "which the question didn't list" ) - unchecked: dict[str, set[str]] = {} + excused: dict[str, set[str]] = {} + for t in set(proposed) | set(columns_add): + took = proposed.get(t, set()) if accepted else set() + excused[t] = (took | columns_add.get(t, set())) - columns_remove.get(t, set()) + # Columns of a new row no rule checks, per kind and table. + open_by_kind: dict[str, dict[str, dict[str, str]]] = {} for p in plans: + own: dict[str, dict[str, str]] = {} if p.mode == "create" and p.demos: for v in [Variant(p.kind, sorted(p.locations))]: for col, role in _anchor_roles(schema, p, v, actor_params).items(): - if role.role in ("varies", "free", "timestamp"): - unchecked.setdefault(p.anchor, set()).add(col) - for g in [*p.children.values(), *p.lists.values()]: - for col, role in g.roles.items(): - if role.role in ("varies", "free", "timestamp"): - unchecked.setdefault(g.table, set()).add(col) - candidates = set().union(*(p.log_candidates for p in plans)) if plans else set() - logs = log_add if accepted else set() - sides = sorted({f"{s.table} ({s.count} per write)" for p in plans for s in p.sides}) - # A table the person calls a log (one the demonstrations added rows to: - # a candidate or a counted side table) is a log for every kind, since - # an app logs every request, not only the kind a demonstration made. - added_to = candidates | {s.table for p in plans for s in p.sides} + if role.role in OPEN_ROLES: + own.setdefault(p.anchor, {})[col] = role.role + for g in [*p.children.values(), *p.lists.values()]: + for col, role in g.roles.items(): + if role.role in OPEN_ROLES: + own.setdefault(g.table, {})[col] = role.role + for sd in p.sides: + for col, role in sd.roles.items(): + if role.role in OPEN_ROLES: + own.setdefault(sd.table, {})[col] = role.role + open_by_kind[p.kind] = own + unchecked: dict[str, set[str]] = {} + for own_cols in open_by_kind.values(): + for t, roles_of in own_cols.items(): + unchecked.setdefault(t, set()).update(roles_of) + for t, cols in unchecked_named.items(): + extra = cols - unchecked.get(t, set()) + if extra: + raise GenerateError( + f"bookkeeping.unchecked names {t}: {', '.join(sorted(extra))}, " + "which the question didn't list" + ) for p in plans: - added_to |= p.logs - logs = logs & added_to + # A column that varies or is the write's time stamp can't be pinned: + # unnamed, the kind has no contract. Free text left empty is pinned + # empty instead. + missing = sorted( + f"{t}.{c}" + for t, roles_of in open_by_kind[p.kind].items() + for c, role in roles_of.items() + if role != "free" and c not in unchecked_named.get(t, set()) + ) + if missing and not p.refused: + p.refused.append( + "new-row columns no rule checks and the answer doesn't name under " + "bookkeeping.unchecked: " + ", ".join(missing) + ) + # Tables a person may name as logs, per kind: those the kind's + # demonstrations added rows to beside the write. + log_offer = { + p.kind: set(p.log_candidates) | {s.table for s in p.sides} for p in plans + } + candidates = set().union(*log_offer.values()) if plans else set() + extra_logs = log_add - candidates + if extra_logs: + raise GenerateError( + "bookkeeping.logs_add names tables the question didn't list: " + + ", ".join(sorted(extra_logs)) + ) + sides = sorted({f"{s.table} ({s.count} per write)" for p in plans for s in p.sides}) for p in plans: # a table the person calls a log is no longer counted - p.sides = [x for x in p.sides if x.table not in logs] - p.logs = set(logs) + p.logs = log_add & log_offer[p.kind] + p.sides = [x for x in p.sides if x.table not in p.logs] unexcused = {t: c for p in plans for t, c in p.unexcused.items()} if proposed or offered or unchecked or candidates or sides or unexcused: lines = [] @@ -2861,19 +3043,20 @@ def propose( ) if candidates: lines.append( - "Tables that gained a different number of rows in different " - "demonstrations; they are not logs unless you list them under " - "logs_add (rows may then be added, existing rows must not change" - + ")" - + ": " + "Tables the app added rows to beside the write. None is a log " + "unless you list it under logs_add (for the kinds whose " + "demonstrations added rows there, rows may then be added any " + "number of times, existing rows must not change): " + ", ".join(sorted(candidates)) + "." ) if unchecked: lines.append( - "Columns of a new record that are not checked (they differ between " + "Columns of a new row no rule checks (they differ between " "demonstrations, are free text every demonstration left empty, or " - "are the write's time stamp): " + "are the write's time stamp). Each is checked only as you name it " + "under unchecked: free text you don't name must stay empty, and a " + "kind with another column you don't name has no contract: " + "; ".join( f"{t}: {', '.join(sorted(c))}" for t, c in sorted(unchecked.items()) ) @@ -2885,9 +3068,16 @@ def propose( "Is this the app's own bookkeeping? " + " ".join(lines) + " Answer `bookkeeping: accept`, or a mapping with any of " - "logs_add, columns_add, columns_remove. Unanswered, nothing is " - "excused and no table is a log.", - "accept", + "accept (true takes the proposed columns), logs_add, columns_add, " + "columns_remove and unchecked. An answer can name only what this " + "question lists. Unanswered, nothing is excused, no table is a " + "log and no column is left unchecked.", + { + "accept": True, + "unchecked": {t: sorted(c) for t, c in sorted(unchecked.items())}, + } + if unchecked + else "accept", ) ) # Q: the recheck window (frame.database.recheck_after_s). @@ -2913,6 +3103,7 @@ def propose( dict(actor_params), recheck or DEFAULT_RECHECK_S, env, + unchecked=unchecked_named, ) # Tables whose name the SQL guard refuses: a view, or a refusal. @@ -3113,7 +3304,9 @@ def _questions_md( for i, q in enumerate(questions, 1): done = _answer(answers, q.key) lines.append(f"{i}. {q.text}") - lines.append(f" - Answer key: `{q.key}`. Proposed: `{q.proposal}`.") + lines.append( + f" - Answer key: `{q.key}`. Proposed: `{json.dumps(q.proposal, ensure_ascii=False)}`." + ) lines.append( f" - {'Answered: `' + json.dumps(done, ensure_ascii=False) + '`' if done is not None else 'Not answered yet.'}" ) diff --git a/packages/flow/openadapt_flow/runtime/effects/frame.py b/packages/flow/openadapt_flow/runtime/effects/frame.py index 96ed499086..aa41a4e0c4 100644 --- a/packages/flow/openadapt_flow/runtime/effects/frame.py +++ b/packages/flow/openadapt_flow/runtime/effects/frame.py @@ -456,6 +456,12 @@ class FrameSpec(BaseModel): #: Fields of a pre-existing target record the save may change. Unset: #: the fields the effects name. target_fields: Optional[list[str]] = None + #: Surface -> the fields a declared row of that surface may change + #: (a database table counts as the surface ``database.views`` maps it + #: to). Set: it replaces ``target_fields`` for every declared row, and a + #: surface it doesn't list may change nothing, so a row touched beside + #: the write can't take the target's fields. + target_fields_by_surface: Optional[dict[str, list[str]]] = None attribution: Optional[FrameAttribution] = None #: Rows the save adds beside each new declared record (``FrameSideRecord``). side_records: list[FrameSideRecord] = Field(default_factory=list) @@ -473,6 +479,8 @@ def _compact(self, handler: Any) -> dict[str, Any]: data: dict[str, Any] = handler(self) if data.get("versions") is None: data.pop("versions", None) + if data.get("target_fields_by_surface") is None: + data.pop("target_fields_by_surface", None) return data @model_validator(mode="after") @@ -839,6 +847,13 @@ def judge_frame( surface_ignore |= set(attribution.server_set) written = declared_fields(effects) may_change = set(spec.target_fields) if spec.target_fields is not None else written + if spec.target_fields_by_surface is not None: + # Every field any declared row may change, for the attribution check. + may_change = ( + set().union(*spec.target_fields_by_surface.values()) + if (spec.target_fields_by_surface) + else set() + ) if attribution is not None and attribution.kind == "record_field": # A field this run writes (or any run parameter names) is not evidence # of who changed a record: refuse the attribution, fail closed. @@ -1323,6 +1338,15 @@ def attribute( concurrent: list[FrameChange] = [] added_by_others: set[tuple[str, str]] = set() views_of = dict(database.views) if database is not None else {} + + def may_change_for(name: str) -> set[str]: + by_surface = spec.target_fields_by_surface + if by_surface is None: + return may_change + if name in by_surface: + return set(by_surface[name]) + return set(by_surface.get(views_of.get(name, ""), [])) + declared_keys: set[str] = set() #: What an audit entry names for each compared record (``ref_of``). refs: dict[tuple[str, str], tuple[Any, Optional[str]]] = {} @@ -1542,12 +1566,13 @@ def partner( if after is None: continue # a removed target: the delta check's case fields = changed(name, before, after) # type: ignore[arg-type] - stray = [f for f in fields if f not in may_change] + row_may = may_change_for(name) + stray = [f for f in fields if f not in row_may] if other_on_target(name, key, before, after): ref, field = ref_of(name, key, before, after) if ( fields - and not (set(fields) & may_change) + and not (set(fields) & row_may) and attribution is not None and attribution.write_events and attribution.table_field diff --git a/packages/flow/openadapt_flow/runtime/effects/versions.py b/packages/flow/openadapt_flow/runtime/effects/versions.py index 01e5a91bb4..1658ee11f4 100644 --- a/packages/flow/openadapt_flow/runtime/effects/versions.py +++ b/packages/flow/openadapt_flow/runtime/effects/versions.py @@ -37,7 +37,7 @@ from dataclasses import dataclass, field from typing import Any, Literal, Optional -from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_serializer, model_validator from openadapt_flow.runtime.effects.effect import ValueExpr @@ -76,6 +76,9 @@ class FrameVersions(BaseModel): #: The target record's field (in the contract's read set) that equals #: ``record_field``. Unset: ``frame.identity_field``, else ``id``. target_field: Optional[str] = None + #: Only records matched on this surface are targets (the main record's + #: surface). Unset: every record a confirmed write effect matched. + target_surface: Optional[str] = None #: The version row's version id (``meta.versionId``, ``res_ver``, an #: audit entry's id). version_field: str = Field(min_length=1) @@ -99,6 +102,15 @@ def _coerce_self(cls, data: Any) -> Any: ] return data + @model_serializer(mode="wrap") + def _compact(self, handler: Any) -> dict[str, Any]: + # Unset, ``target_surface`` is left out, so a contract that doesn't + # use it keeps its digests. + data: dict[str, Any] = handler(self) + if data.get("target_surface") is None: + data.pop("target_surface", None) + return data + @model_validator(mode="after") def _shape(self) -> "FrameVersions": if bool(self.author_field) != bool(self.self_actors): diff --git a/packages/flow/openadapt_flow/verify_run.py b/packages/flow/openadapt_flow/verify_run.py index 7b82266f5e..2d4ad30797 100644 --- a/packages/flow/openadapt_flow/verify_run.py +++ b/packages/flow/openadapt_flow/verify_run.py @@ -496,10 +496,29 @@ def _judge_versions( # change, never by the target field's value after the write: a target # field the write changed would make an old record look created. identity = cfg.frame.identity_field or "id" # type: ignore[union-attr] - old: dict[str, dict[str, Any]] = {} - for r in before.records: - if r.get(identity) not in (None, ""): - old.setdefault(str(r.get(identity)), dict(r)) + from openadapt_flow.runtime.effects.surfaces import surface_of + + def index(rows: Sequence[Mapping[str, Any]]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for r in rows: + if r.get(identity) not in (None, ""): + out.setdefault(str(r.get(identity)), dict(r)) + return out + + def before_rows(effect: Effect) -> Optional[dict[str, dict[str, Any]]]: + """The records read before the write where this effect looks: its + surface's own read, else the verifier's records. None: no read says + which records were there (then no target is taken as new).""" + + surface = surface_of(effect) + if surface: + sub = (before.detail or {}).get(surface) + if not isinstance(sub, Mapping) or not sub.get("reachable"): + return None + rows = sub.get("records") + return index(rows) if isinstance(rows, list) else None + return index(before.records) if before.reachable else None + ignored = set(cfg.frame.ignore_fields) # type: ignore[union-attr] targets: list[tuple[Optional[str], bool, bool]] = [] for effect, verdict in zip(effects, verdicts): @@ -509,13 +528,20 @@ def _judge_versions( continue # a guard: "nothing here" if not verdict.confirmed: continue + if ( + spec.target_surface is not None + and surface_of(effect) != spec.target_surface + ): + continue # a row beside the target, not the target + old = before_rows(effect) for record in verdict.matched_records: value = record.get(field_name) text = None if value in (None, "") else str(value) key = record.get(identity) - was = None if key in (None, "") else old.get(str(key)) - # No identity to look up: never assume the record is new. - existed = was is not None or key in (None, "") + was = None if key in (None, "") or old is None else old.get(str(key)) + # A record is new only when the read before the write shows it + # wasn't there. No identity, or no read to look in: never new. + existed = was is not None or key in (None, "") or old is None # Changed in the window: a field read before differs now (a # sealed baseline marks a changed field, so it differs too). changed = existed and ( diff --git a/packages/flow/tests/test_generate_verify_contract.py b/packages/flow/tests/test_generate_verify_contract.py index 9bd166e70e..3806f18152 100644 --- a/packages/flow/tests/test_generate_verify_contract.py +++ b/packages/flow/tests/test_generate_verify_contract.py @@ -294,13 +294,17 @@ def demo(kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None: "--out", str(work / "out"), "--actor-param", f"bot_user_id={BOT}"] # fmt: skip assert gen.main(argv) == 0 report = json.loads((work / "out" / "report.json").read_text()) - # The person accepts the proposals. - # The person accepts the proposals, and says journal_details (one row - # per changed field) is the app's log. answers = {q["key"]: q["proposal"] for q in report["questions"]} - answers["bookkeeping"] = {"logs_add": ["journal_details"]} + # The person takes the proposal (each unchecked column named) and names + # journal_details a log. + proposal = answers["bookkeeping"] + answers["bookkeeping"] = { + **(proposal if isinstance(proposal, dict) else {"accept": True}), + "logs_add": ["journal_details"], + } (work / "answers.yaml").write_text(yaml.safe_dump(answers)) assert gen.main([*argv, "--answers", str(work / "answers.yaml")]) == 0 + report = json.loads((work / "out" / "report.json").read_text()) if os.environ.get("OA_GEN_TEST_KEEP"): shutil.copytree(work, os.environ["OA_GEN_TEST_KEEP"], dirs_exist_ok=True) return {"app": app, "out": work / "out", "report": report, "read": read} @@ -336,15 +340,19 @@ def test_questions_and_contracts(generated: dict[str, Any]) -> None: kinds = report["kinds"] assert kinds["create_issue"]["mode"] == "create" assert kinds["update_issue"]["mode"] == "update" - assert kinds["create_issue"]["emitted"] > 0, kinds["create_issue"] + assert kinds["create_issue"]["emitted"] > 0, ( + kinds["create_issue"]["refused"], + kinds["create_issue"]["rejected"][:1], + ) # Four change fields the examples give (status, assignee, done ratio, # notes), and the due date that create_issue's demonstrations placed on # the same table: every non-empty subset gets a contract. assert kinds["update_issue"]["placed_from_another_kind"] == ["due_date"] - # The journal names the issue and the user who saved it: the update's - # contracts read it as the target's versions (a lost update shows there). - assert kinds["update_issue"]["versions"]["kind"] == "audit" - assert kinds["update_issue"]["versions"]["table"] == "journals" + # lock_version steps by one per save: the update's contracts read it as + # the target's versions (a lost update shows there). It is preferred to + # the journals, which an issue saved before them doesn't have. + assert kinds["update_issue"]["versions"]["kind"] == "step" + assert kinds["update_issue"]["versions"]["column"] == "lock_version" assert kinds["update_issue"]["emitted"] == 31, json.dumps( kinds["update_issue"], indent=1 ) diff --git a/packages/flow/tests/test_generate_verify_contract_attacks_r2.py b/packages/flow/tests/test_generate_verify_contract_attacks_r2.py new file mode 100644 index 0000000000..185077ef20 --- /dev/null +++ b/packages/flow/tests/test_generate_verify_contract_attacks_r2.py @@ -0,0 +1,423 @@ +# ruff: noqa: F811 (pytest fixtures imported from the generator test, then used as arguments) +"""The adversarial review's round-2 attacks on generated contracts (#1672). + +An issue tracker that audits one row per changed field and touches the +issue's project on every save, on a throwaway PostgreSQL. Every wrong write +must stop; the correct writes beside them must be CONFIRMED. Also: the +bookkeeping answer can name only what its question offered, and the +PostgreSQL read-only check counts inherited and non-DML privileges. Records +are made up. +""" + +from __future__ import annotations + +import json +import os +from typing import Any + +import pytest +import yaml + +psycopg2 = pytest.importorskip("psycopg2") + +from openadapt_flow.generate import verify_contract as gen # noqa: E402 +from tests.test_generate_verify_contract import ( # noqa: E402,F401 + BOT, + SCHEMA, + App, + _run, + pg, +) + +SCHEMA_V = ( + SCHEMA.replace( + "CREATE TABLE projects (id serial PRIMARY KEY, name text NOT NULL);", + "CREATE TABLE projects (id serial PRIMARY KEY, name text NOT NULL, " + "description text, updated_on timestamp);", + ) + .replace( + # No version column: the versions come from the audit trail. + "lock_version integer NOT NULL DEFAULT 0);", + "description text, parent_id integer);", + ) + .replace( + "CREATE TABLE settings", + "CREATE TABLE audits (id serial PRIMARY KEY, issue_id integer, user_id integer, " + "action text, field_name text, old_value text, new_value text);\nCREATE TABLE settings", + ) + .replace("CREATE ROLE oa_reader LOGIN PASSWORD 'reader-pw';", "") +) + + +class AppV(App): + """Audits one row per changed field; touches the project on every save + (a parent ``touch``); writes no journals and has no version column.""" + + def create_issue(self, v: dict[str, Any], *, project_id: Any = None) -> int: + pid = project_id or self.one( + "SELECT id FROM projects WHERE name=%s", (v["project"],) + ) + sid = self.one("SELECT id FROM statuses WHERE name=%s", (v["status"],)) + parent = None + if v.get("parent"): + parent = v.get("parent_id") or self.one( + "SELECT i.id FROM issues i WHERE i.subject=%s ORDER BY id LIMIT 1", + (v["parent"],), + ) + iid = self.one( + "INSERT INTO issues (project_id, subject, status_id, author_id, created_on, updated_on, parent_id) " + "VALUES (%s,%s,%s,%s,now(),now(),%s) RETURNING id", + (pid, v["subject"], sid, BOT, parent), + ) + self.q( + "INSERT INTO audits (issue_id, user_id, action) VALUES (%s,%s,'create')", + (iid, BOT), + ) + self.q("UPDATE projects SET updated_on=now() WHERE id=%s", (pid,)) + return iid # fmt: skip + + def save(self, iid: int, sets: dict[str, Any], *, user: int = BOT) -> None: + for col, val in sets.items(): + if col == "status": + col, val = ( + "status_id", + self.one("SELECT id FROM statuses WHERE name=%s", (val,)), + ) + old = self.one(f"SELECT {col}::text FROM issues WHERE id=%s", (iid,)) + self.q(f"UPDATE issues SET {col}=%s WHERE id=%s", (val, iid)) + self.q( + "INSERT INTO audits (issue_id, user_id, action, field_name, old_value, new_value) " + "VALUES (%s,%s,'update',%s,%s,%s)", + (iid, user, col, old, None if val is None else f"v{val}"), + ) + self.q("UPDATE issues SET updated_on=now() WHERE id=%s", (iid,)) + self.q("UPDATE projects SET updated_on=now() WHERE id=(SELECT project_id FROM issues WHERE id=%s)", + (iid,)) # fmt: skip + + +KINDS_V = { + "kinds": [ + {"kind": "create_issue", "parameters": ["project", "subject", "status", "parent"], + "examples": [ + {"values": {"project": "Help Desk", "subject": "Toner low", "status": "New", "parent": "Printer jams"}}, + {"values": {"project": "Billing Portal", "subject": "VPN cert", "status": "New", "parent": "VPN drops"}}, + ]}, + {"kind": "set_status", "parameters": ["project", "subject", "status"], + "examples": [ + {"values": {"project": "Help Desk", "subject": "Printer jams", "status": "In Progress"}}, + {"values": {"project": "Help Desk", "subject": "VPN drops", "status": "Resolved"}}, + ]}, + {"kind": "edit_description", "parameters": ["project", "subject", "description"], + "examples": [ + {"values": {"project": "Help Desk", "subject": "VPN drops", "description": "Drops every hour"}}, + {"values": {"project": "Billing Portal", "subject": "Refund button missing", + "description": "Only on Safari"}}, + ]}, + ] +} # fmt: skip + + +def _propose(work: Any, argv: list[str], answers: Any) -> int: + (work / "answers.yaml").write_text(yaml.safe_dump(answers)) + return gen.main([*argv, "--answers", str(work / "answers.yaml")]) + + +@pytest.fixture(scope="module") +def gv(pg: Any) -> dict[str, Any]: + root, port = pg["root"], pg["port"] + admin = psycopg2.connect( + host="127.0.0.1", port=port, user="admin", dbname="postgres" + ) + admin.autocommit = True + admin.cursor().execute("CREATE DATABASE trackerv") + admin.close() + app = AppV(port, "trackerv") + with app.con() as c, c.cursor() as cur: + cur.execute( + "DO $$BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='oa_reader') " + "THEN CREATE ROLE oa_reader LOGIN PASSWORD 'reader-pw'; END IF; END$$" + ) + cur.execute(SCHEMA_V) + os.environ["OA_GEN_TEST_PW"] = "reader-pw" + work = root / "workv" + work.mkdir() + read = {"sql_driver": "psycopg2", + "sql_connect_args": {"host": "127.0.0.1", "port": port, "user": "oa_reader", "dbname": "trackerv"}, + "sql_password_env": "OA_GEN_TEST_PW"} # fmt: skip + (work / "read.yaml").write_text(yaml.safe_dump(read)) + (work / "kinds.yaml").write_text(yaml.safe_dump(KINDS_V, allow_unicode=True)) + common = ["--read", str(work / "read.yaml")] + + def demo(kind: str, values: dict[str, Any], write: Any) -> None: + assert gen.main(["snapshot", *common, "--out", str(work / "before.json")]) == 0 + write() + assert gen.main(["record", *common, "--before", str(work / "before.json"), "--kind", kind, + "--values", json.dumps(values), "--demos", str(work / "demos")]) == 0 # fmt: skip + + for k in KINDS_V["kinds"]: + for ex in k["examples"]: + v = ex["values"] + if k["kind"] == "create_issue": + demo("create_issue", v, lambda v=v: app.create_issue(v)) + else: + sets = {p: v[p] for p in k["parameters"][2:] if v.get(p) is not None} + demo( + k["kind"], + v, + lambda v=v, s=sets: app.save( + app.find(v["project"], v["subject"]), s + ), + ) + argv = ["propose", *common, "--kinds", str(work / "kinds.yaml"), "--demos", str(work / "demos"), + "--out", str(work / "out"), "--actor-param", f"bot_user_id={BOT}"] # fmt: skip + assert gen.main(argv) == 0 + report = json.loads((work / "out" / "report.json").read_text()) + answers = {q["key"]: q["proposal"] for q in report["questions"]} + assert _propose(work, argv, answers) == 0 + report = json.loads((work / "out" / "report.json").read_text()) + return {"app": app, "out": work / "out", "report": report, "read": read, + "work": work, "argv": argv, "answers": answers} # fmt: skip + + +def _fresh(app: AppV, subject: str) -> int: + return app.create_issue( + {"project": "Help Desk", "subject": subject, "status": "New"} + ) + + +def test_versions_read_write_events_of_creates_and_updates(gv: dict[str, Any]) -> None: + v = gv["report"]["kinds"]["edit_description"]["versions"] + assert v["kind"] == "audit" and v["table"] == "audits", v + # Compared only with creates; a column that varies with the fields saved + # (field_name) is never a filter. + assert v["where"] == {"action": ["create", "update"]}, v + + +def test_correct_status_update_confirmed(gv: dict[str, Any]) -> None: + app = gv["app"] + iid = _fresh(app, "V1 control") + v = {"project": "Help Desk", "subject": "V1 control", "status": "Resolved"} + out = _run(gv, "set_status", v, lambda: app.save(iid, {"status": "Resolved"})) + assert out.startswith("CONFIRMED"), out + + +def test_correct_description_edit_confirmed(gv: dict[str, Any]) -> None: + app = gv["app"] + iid = _fresh(app, "T1 control") + v = {"project": "Help Desk", "subject": "T1 control", "description": "Fixed"} + out = _run( + gv, "edit_description", v, lambda: app.save(iid, {"description": "Fixed"}) + ) + assert out.startswith("CONFIRMED"), out + + +def test_lost_update_of_another_field_stops(gv: dict[str, Any]) -> None: + """Another user sets the due date; the agent's stale save puts it back.""" + app = gv["app"] + iid = _fresh(app, "V4 lost update") + v = {"project": "Help Desk", "subject": "V4 lost update", "description": "Fixed"} + + def write() -> None: + app.save(iid, {"due_date": "2026-12-31"}, user=2) + app.save(iid, {"description": "Fixed", "due_date": None}) + + out = _run(gv, "edit_description", v, write) + assert not out.startswith("CONFIRMED"), out + + +def test_lost_update_of_the_same_field_stops(gv: dict[str, Any]) -> None: + app = gv["app"] + iid = _fresh(app, "V5") + v = {"project": "Help Desk", "subject": "V5", "description": "Fixed"} + + def write() -> None: + app.save(iid, {"description": "Theirs"}, user=2) + app.save(iid, {"description": "Fixed"}) + + out = _run(gv, "edit_description", v, write) + assert not out.startswith("CONFIRMED"), out + + +def test_target_without_a_baseline_version_stops(gv: dict[str, Any]) -> None: + """An issue with no audit row at all (written outside the app) can't be + shown to be new: with no version before the write it stops as missing.""" + app = gv["app"] + iid = _fresh(app, "No history") + app.q("DELETE FROM audits WHERE issue_id=%s", (iid,)) + v = {"project": "Help Desk", "subject": "No history", "description": "Fixed"} + out = _run( + gv, "edit_description", v, lambda: app.save(iid, {"description": "Fixed"}) + ) + assert not out.startswith("CONFIRMED"), out + + +def test_touched_parent_row_cant_take_the_targets_field(gv: dict[str, Any]) -> None: + app = gv["app"] + iid = _fresh(app, "T2 leak") + v = {"project": "Help Desk", "subject": "T2 leak", "description": "Fixed"} + + def write() -> None: + app.save(iid, {"description": "Fixed"}) + app.q( + "UPDATE projects SET description='Overwritten by the agent' WHERE name='Help Desk'" + ) + + try: + out = _run(gv, "edit_description", v, write) + finally: + app.q("UPDATE projects SET description=NULL WHERE name='Help Desk'") + assert not out.startswith("CONFIRMED"), out + + +def test_target_cant_take_the_touched_rows_field(gv: dict[str, Any]) -> None: + """A column nothing excuses on the target (is_private) stops, beside a + correct save.""" + app = gv["app"] + iid = _fresh(app, "T4") + v = {"project": "Help Desk", "subject": "T4", "description": "Fixed"} + + def write() -> None: + app.save(iid, {"description": "Fixed"}) + app.q("UPDATE issues SET is_private=true WHERE id=%s", (iid,)) + + out = _run(gv, "edit_description", v, write) + assert not out.startswith("CONFIRMED"), out + + +def test_correct_create_with_parent_confirmed(gv: dict[str, Any]) -> None: + app = gv["app"] + v = { + "project": "Help Desk", + "subject": "P0 child", + "status": "New", + "parent": "VPN drops", + } + out = _run(gv, "create_issue", v, lambda: app.create_issue(v)) + assert out.startswith("CONFIRMED"), out + + +def test_parent_named_by_a_subject_two_issues_share_stops(gv: dict[str, Any]) -> None: + """'Refund button missing' names two issues: a key back to the main table + must name exactly one row too.""" + app = gv["app"] + legacy = app.find("Billing Portal Legacy", "Refund button missing") + v = { + "project": "Help Desk", + "subject": "P1 child", + "status": "New", + "parent": "Refund button missing", + } + out = _run( + gv, "create_issue", v, lambda: app.create_issue({**v, "parent_id": legacy}) + ) + assert not out.startswith("CONFIRMED"), out + + +# -- answers name only what the question offered -------------------------------- + + +def _offered(gv: dict[str, Any]) -> Any: + return dict(gv["answers"]) + + +def test_answer_with_an_unknown_key_is_refused(gv: dict[str, Any], capsys: Any) -> None: + answers = _offered(gv) + answers["bookkeeping"] = {"accept": True, "also_excuse": {"issues": ["subject"]}} + assert _propose(gv["work"], gv["argv"], answers) != 0 + assert "unknown answer keys" in capsys.readouterr().err + + +def test_logs_add_of_an_unlisted_table_is_refused( + gv: dict[str, Any], capsys: Any +) -> None: + answers = _offered(gv) + answers["bookkeeping"] = {"accept": True, "logs_add": ["issues"]} + assert _propose(gv["work"], gv["argv"], answers) != 0 + assert "logs_add names tables" in capsys.readouterr().err + + +def test_columns_add_of_an_unlisted_column_is_refused( + gv: dict[str, Any], capsys: Any +) -> None: + answers = _offered(gv) + answers["bookkeeping"] = {"accept": True, "columns_add": {"issues": ["subject"]}} + assert _propose(gv["work"], gv["argv"], answers) != 0 + assert "columns_add names" in capsys.readouterr().err + + +def test_a_mapping_without_accept_takes_no_proposed_column(gv: dict[str, Any]) -> None: + """``{logs_add: [...]}`` alone doesn't accept the proposed bookkeeping: + the save's updated_on then isn't excused and a correct update stops.""" + answers = _offered(gv) + answers["bookkeeping"] = {"logs_add": ["audits"]} + assert _propose(gv["work"], gv["argv"], answers) == 0 + try: + app = gv["app"] + iid = _fresh(app, "No accept") + v = {"project": "Help Desk", "subject": "No accept", "status": "Resolved"} + out = _run(gv, "set_status", v, lambda: app.save(iid, {"status": "Resolved"})) + assert not out.startswith("CONFIRMED"), out + finally: + assert _propose(gv["work"], gv["argv"], gv["answers"]) == 0 + + +def test_unnamed_unchecked_columns_refuse_the_kind(gv: dict[str, Any]) -> None: + """A new row's time stamp named nowhere: no contract, so the write stops.""" + answers = _offered(gv) + proposal = answers["bookkeeping"] + assert isinstance(proposal, dict) and proposal.get("unchecked"), proposal + answers["bookkeeping"] = {"accept": True} + try: + assert _propose(gv["work"], gv["argv"], answers) == 0 + report = json.loads((gv["out"] / "report.json").read_text()) + refused = " ".join(report["kinds"]["create_issue"]["refused"]) + assert "bookkeeping.unchecked" in refused, report["kinds"]["create_issue"] + app = gv["app"] + v = { + "project": "Help Desk", + "subject": "No contract", + "status": "New", + "parent": "VPN drops", + } + with pytest.raises(gen.GenerateError): + _run(gv, "create_issue", v, lambda: app.create_issue(v)) + finally: + assert _propose(gv["work"], gv["argv"], gv["answers"]) == 0 + + +# -- PostgreSQL read-only check ---------------------------------------------------- + + +def test_pg_read_only_check_counts_inherited_and_non_dml_privileges(pg: Any) -> None: + port = pg["port"] + admin = psycopg2.connect( + host="127.0.0.1", port=port, user="admin", dbname="postgres" + ) + admin.autocommit = True + admin.cursor().execute("CREATE DATABASE rotest") + admin.close() + a = psycopg2.connect(host="127.0.0.1", port=port, user="admin", dbname="rotest") + a.autocommit = True + a.cursor().execute( + "CREATE TABLE t (id serial primary key, v text); CREATE ROLE writers; " + "GRANT INSERT, UPDATE, DELETE ON t TO writers; " + "CREATE ROLE ro1 LOGIN; GRANT SELECT ON t TO ro1; GRANT writers TO ro1; " + "CREATE ROLE ro2 LOGIN; GRANT SELECT, TRIGGER ON t TO ro2; GRANT CREATE ON SCHEMA public TO ro2; " + "CREATE ROLE ro3 LOGIN; GRANT SELECT ON t TO ro3; GRANT SELECT ON t_id_seq TO ro3;" + ) # fmt: skip + a.close() + + def check(role: str) -> list[str]: + return gen.check_read_only( + lambda: psycopg2.connect( + host="127.0.0.1", port=port, user=role, dbname="rotest" + ), + "postgresql", + ) + + ro1 = check("ro1") + assert any("INSERT on table public.t" in p for p in ro1), ro1 + ro2 = check("ro2") + assert any("TRIGGER on table public.t" in p for p in ro2), ro2 + assert any("CREATE on schema public" in p for p in ro2), ro2 + assert check("ro3") == [] From cc3294584a7829ce8dbcd109aab7b5d5ad1f1383 Mon Sep 17 00:00:00 2001 From: abrichr Date: Sat, 31 Oct 2026 00:08:47 -0400 Subject: [PATCH 89/94] Bookkeeping proposal names only columns that vary or are time stamps; free text stays pinned empty --- packages/flow/docs/GENERATE_CONTRACTS.md | 5 +++-- .../openadapt_flow/generate/verify_contract.py | 16 ++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index d7e6f31abe..4a9a780cf4 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -198,8 +198,9 @@ that differ between demonstrations, free text every demonstration left empty, and the write's time stamps are listed in the bookkeeping question, and each is left unchecked only if the answer names it (`unchecked`). Free text the answer doesn't name must stay empty; a kind with any other column -the answer doesn't name gets no contract. The proposed answer names every -listed column, so accepting it is explicit. +the answer doesn't name gets no contract. The proposed answer names each +column that varies or is a time stamp, so accepting it is explicit, and +leaves free text out, so that stays empty. **Rows the app adds on its own.** A row type the app added the same number of times in every demonstration (an agenda event, a stream note, a price-history diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 12afb98125..7a05af080a 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -2963,6 +2963,13 @@ def table_cols(key: str) -> dict[str, set[str]]: for own_cols in open_by_kind.values(): for t, roles_of in own_cols.items(): unchecked.setdefault(t, set()).update(roles_of) + must_name: dict[str, list[str]] = {} + for own_cols in open_by_kind.values(): + for t, roles_of in own_cols.items(): + for c, role_name in roles_of.items(): + if role_name != "free" and c not in must_name.get(t, []): + must_name.setdefault(t, []).append(c) + must_name = {t: sorted(c) for t, c in sorted(must_name.items())} for t, cols in unchecked_named.items(): extra = cols - unchecked.get(t, set()) if extra: @@ -3072,12 +3079,9 @@ def table_cols(key: str) -> dict[str, set[str]]: "columns_remove and unchecked. An answer can name only what this " "question lists. Unanswered, nothing is excused, no table is a " "log and no column is left unchecked.", - { - "accept": True, - "unchecked": {t: sorted(c) for t, c in sorted(unchecked.items())}, - } - if unchecked - else "accept", + # The proposal names only the columns that vary or are time + # stamps: free text left empty is better pinned empty. + {"accept": True, "unchecked": must_name} if must_name else "accept", ) ) # Q: the recheck window (frame.database.recheck_after_s). From 893a613d7db49651dde08a7721541732544bdf1d Mon Sep 17 00:00:00 2001 From: abrichr Date: Sat, 31 Oct 2026 00:24:59 -0400 Subject: [PATCH 90/94] Free text of a row the app adds is pinned empty (NULL or ''), not to its default --- .../flow/openadapt_flow/generate/verify_contract.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 7a05af080a..010038bdb9 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -2200,10 +2200,17 @@ def _raw_effect( or (role.detail or {}).get("is_default") ): if role is not None and role.role in OPEN_ROLES: - # Set freely only when the person named it unchecked; else - # it must hold its default. + # Set freely only when the person named it unchecked. Free + # text not named must be empty (NULL or ''): pinned so, by the + # rows' own match. Anything else must hold its default. if c.name in settings.unchecked.get(table, set()): nrf.add(c.name) + elif role.role == "free": + alias = _add_column( + surf, f"empty__{_alias(c.name)}", b.empty("t", table, c.name) + ) + m[alias] = {"literal": "1"} + nrf.add(c.name) continue if role is None or role.role in ("key", "link", "copied", "actor"): nrf.add(c.name) From 2bc534fcb7dc18a219509418365e38553f9a9f86 Mon Sep 17 00:00:00 2001 From: abrichr Date: Sat, 31 Oct 2026 00:29:02 -0400 Subject: [PATCH 91/94] Bookkeeping proposal names every open column; rows the app adds keep their defaults when a column isn't named --- packages/flow/docs/GENERATE_CONTRACTS.md | 9 +++--- .../generate/verify_contract.py | 30 +++++++------------ 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index 4a9a780cf4..af2734d917 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -197,10 +197,11 @@ task gives, and a generated token (a UUID) are recognized as such. Columns that differ between demonstrations, free text every demonstration left empty, and the write's time stamps are listed in the bookkeeping question, and each is left unchecked only if the answer names it (`unchecked`). Free -text the answer doesn't name must stay empty; a kind with any other column -the answer doesn't name gets no contract. The proposed answer names each -column that varies or is a time stamp, so accepting it is explicit, and -leaves free text out, so that stays empty. +text the answer doesn't name must stay empty on the main record and its +child rows, and hold its default on a row the app adds beside the write; a +kind with any other column the answer doesn't name gets no contract. The +proposed answer names every listed column, so accepting it is explicit; a +person leaves a column out to have it checked. **Rows the app adds on its own.** A row type the app added the same number of times in every demonstration (an agenda event, a stream note, a price-history diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 010038bdb9..9a7d34126b 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -2200,17 +2200,11 @@ def _raw_effect( or (role.detail or {}).get("is_default") ): if role is not None and role.role in OPEN_ROLES: - # Set freely only when the person named it unchecked. Free - # text not named must be empty (NULL or ''): pinned so, by the - # rows' own match. Anything else must hold its default. + # Set freely only when the person named it unchecked; else + # it must hold its default (the frame reads these rows from + # the table itself, so no computed pin applies). if c.name in settings.unchecked.get(table, set()): nrf.add(c.name) - elif role.role == "free": - alias = _add_column( - surf, f"empty__{_alias(c.name)}", b.empty("t", table, c.name) - ) - m[alias] = {"literal": "1"} - nrf.add(c.name) continue if role is None or role.role in ("key", "link", "copied", "actor"): nrf.add(c.name) @@ -2970,13 +2964,6 @@ def table_cols(key: str) -> dict[str, set[str]]: for own_cols in open_by_kind.values(): for t, roles_of in own_cols.items(): unchecked.setdefault(t, set()).update(roles_of) - must_name: dict[str, list[str]] = {} - for own_cols in open_by_kind.values(): - for t, roles_of in own_cols.items(): - for c, role_name in roles_of.items(): - if role_name != "free" and c not in must_name.get(t, []): - must_name.setdefault(t, []).append(c) - must_name = {t: sorted(c) for t, c in sorted(must_name.items())} for t, cols in unchecked_named.items(): extra = cols - unchecked.get(t, set()) if extra: @@ -3086,9 +3073,14 @@ def table_cols(key: str) -> dict[str, set[str]]: "columns_remove and unchecked. An answer can name only what this " "question lists. Unanswered, nothing is excused, no table is a " "log and no column is left unchecked.", - # The proposal names only the columns that vary or are time - # stamps: free text left empty is better pinned empty. - {"accept": True, "unchecked": must_name} if must_name else "accept", + # The proposal names every listed column, so accepting it is + # explicit; a person may leave any out to have it checked. + { + "accept": True, + "unchecked": {t: sorted(c) for t, c in sorted(unchecked.items())}, + } + if unchecked + else "accept", ) ) # Q: the recheck window (frame.database.recheck_after_s). From abaa14c2f21caf4bb80c78403b442a9473ef73e9 Mon Sep 17 00:00:00 2001 From: abrichr Date: Sat, 31 Oct 2026 01:42:19 -0400 Subject: [PATCH 92/94] Round 3 review: free text stays pinned, read-code deny-list, SET ROLE and column grants, audit key - The bookkeeping proposal leaves out free text every demonstration left empty; it stays pinned empty unless the person adds it to `unchecked`. On a row the app adds, it holds the empty value the demonstrations stored. - Versions read from an audit trail drop only event codes shown to be reads (logged on a record the demonstration didn't change); an unseen or NULL code counts as a save. - PostgreSQL check_read_only: every role the account can SET ROLE to, and column-level INSERT, UPDATE and REFERENCES grants. - The audit table's own primary key is never a candidate record column. - Docs: a version column is trusted to step from the stored value; an app with an audit trail can read it as well. --- packages/flow/docs/GENERATE_CONTRACTS.md | 41 ++- .../openadapt_flow/generate/demo_analysis.py | 10 +- .../generate/verify_contract.py | 184 ++++++---- ...est_generate_verify_contract_attacks_r2.py | 7 +- ...est_generate_verify_contract_attacks_r3.py | 320 ++++++++++++++++++ 5 files changed, 467 insertions(+), 95 deletions(-) create mode 100644 packages/flow/tests/test_generate_verify_contract_attacks_r3.py diff --git a/packages/flow/docs/GENERATE_CONTRACTS.md b/packages/flow/docs/GENERATE_CONTRACTS.md index af2734d917..b4d12cc9a5 100644 --- a/packages/flow/docs/GENERATE_CONTRACTS.md +++ b/packages/flow/docs/GENERATE_CONTRACTS.md @@ -197,11 +197,13 @@ task gives, and a generated token (a UUID) are recognized as such. Columns that differ between demonstrations, free text every demonstration left empty, and the write's time stamps are listed in the bookkeeping question, and each is left unchecked only if the answer names it (`unchecked`). Free -text the answer doesn't name must stay empty on the main record and its -child rows, and hold its default on a row the app adds beside the write; a -kind with any other column the answer doesn't name gets no contract. The -proposed answer names every listed column, so accepting it is explicit; a -person leaves a column out to have it checked. +text the answer doesn't name must stay empty: on the main record and its +child rows NULL or `''`, and on a row the app adds beside the write the +empty value every demonstration stored there. A kind with any other column +the answer doesn't name gets no contract. The proposed answer names the +columns that vary or are time stamps, so accepting it is explicit, and +leaves free text out: a person adds a free-text column to `unchecked` only +on purpose. **Rows the app adds on its own.** A row type the app added the same number of times in every demonstration (an agenda event, a stream note, a price-history @@ -247,16 +249,20 @@ fails and no contract is written. user's save to the target inside the window stops the write. The generator first takes a whole-number column named as a version that went up by one in every demonstration (Redmine's `lock_version`): every existing record has a -version there. Otherwise it takes the app's own audit trail when every update -demonstration added a row there that names the record and the acting user -(EspoCRM's `action_history_record`, Dolibarr's agenda events). The read is narrowed to the record's type and to write events: a -text column, not an id, that holds one code of letters and underscores in -every row any update kind added (so it doesn't vary with the fields saved) -and only other codes in the rows create kinds added (EspoCRM's `action`). -The read keeps the update code and the create codes, so a record the app -created has a first version, and another user only viewing the record -doesn't count. A time stamp or a token the demonstrations happened to share -is never used. Only the main record is a target (`target_surface`). A target +version there. Such a column is trusted to step from the stored value on +every save. An app that writes back the version its form carried, without +checking it, makes another user's save and the agent's stale one a single +step, which this check can't see. An app with an audit trail of saves can +read that trail as well (a hand-written `frame.versions`), since authors +catch what a step count can't. Otherwise it takes the app's own audit trail +when every update demonstration added a row there that names the record and +the acting user (EspoCRM's `action_history_record`, Dolibarr's agenda +events); the audit table's own key never counts as the record's. The read +is narrowed to the record's type, and every entry counts as a save except +an event code shown to be a read: one a demonstration logged on a record it +didn't add or change, and no demonstration logged on one it did. A code no +demonstration showed (a bulk edit, an import) and a NULL code count as +saves. Only the main record is a target (`target_surface`). A target the read before the write doesn't show as new is treated as existing, and with no version before the write it stops as `missing` (a record saved before the trail began, or outside the app). With neither, the update has no @@ -274,8 +280,9 @@ The generator never leaves a step for a person to apply later. A contract that needs something the store doesn't have is either set up, with a flag and an admin connection you give, or not written, with the reason. -The read account must be read-only. On PostgreSQL the check uses its -effective privileges, so a role it is a member of counts: `INSERT`, +The read account must be read-only. On PostgreSQL the check uses the +privileges of every role it can act as (a role it inherits, or +one it can `SET ROLE` to), and column grants count too: `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, `TRIGGER` or `REFERENCES` on any table, `USAGE` or `UPDATE` on a sequence, or `CREATE` on a schema or the database stops the generator. diff --git a/packages/flow/openadapt_flow/generate/demo_analysis.py b/packages/flow/openadapt_flow/generate/demo_analysis.py index 6b37d78234..3f54acd89f 100644 --- a/packages/flow/openadapt_flow/generate/demo_analysis.py +++ b/packages/flow/openadapt_flow/generate/demo_analysis.py @@ -628,7 +628,15 @@ def column_roles( # Free text the task doesn't give and the demonstrations left # empty (a line's description, a note): an agent may fill it # from the record it copies, so it isn't checked. - out[name] = ColumnRole("free") + # The empty value the demonstrations stored: NULL, '' or both. + empty = ( + "null" + if distinct == {None} + else "text" + if distinct == {""} + else "mixed" + ) + out[name] = ColumnRole("free", detail={"empty": empty}) continue if len(distinct) == 1: v = values[0] diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index 9a7d34126b..bdd2f58bd4 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1144,7 +1144,9 @@ def _audit_source( for ch in d.diff.of(name, "added"): r = ch.after or {} for lc, lv in r.items(): - if lv != key: + # The audit row's own id never names the record, even + # when it happens to equal the record's id. + if lv != key or lc in table.primary_key: continue for ac, av in r.items(): if ac != lc and av in actors: @@ -1178,20 +1180,29 @@ def _audit_source( v = next(iter(values)) if v is not None and v.lower() in names and v.isidentifier(): where[col.name] = v - # Write events only (another user only viewing the record doesn't - # count): a code column (letters and underscores, not an id) that - # holds one value in every row any update kind's demonstrations - # added here, so it doesn't vary with the fields saved, and only - # other codes in the rows create kinds added. The read keeps those - # codes, the update's and the creates' (a record the app created has - # its first version then). A time stamp, a token or a text the - # demonstrations happened to share is never used. - updated = [ - ch.after or {} for d in update_demos for ch in d.diff.of(name, "added") - ] - created = [ - ch.after or {} for d in create_demos for ch in d.diff.of(name, "added") - ] + # Every entry counts as a save except a code shown to be a read: a + # demonstration logged it on a record of this table it didn't add or + # change, and no demonstration logged it on one it did. An unseen + # code (a bulk edit, an import) or a NULL one counts as a save. + pk = t.primary_key[0] + reads: dict[str, set[str]] = {} + writes: dict[str, set[str]] = {} + for d in [*create_demos, *update_demos]: + moved = { + str((ch.after or ch.before or {}).get(pk)) + for kind in ("added", "changed") + for ch in d.diff.of(plan.anchor, kind) + } + for ch in d.diff.of(name, "added"): + r = ch.after or {} + rid = r.get(lc) + if rid is None: + continue + side = writes if str(rid) in moved else reads + for col in table.columns: + v = r.get(col.name) + if v is not None: + side.setdefault(col.name, set()).add(v) for col in table.columns: if ( col.name in (lc, ac) @@ -1201,18 +1212,9 @@ def _audit_source( or col.name in where ): continue - mine = {r.get(col.name) for r in updated} - theirs = {r.get(col.name) for r in created} - codes = mine | theirs - if ( - len(mine) == 1 - and theirs - and not (mine & theirs) - and all( - x is not None and re.fullmatch(r"[A-Za-z_]{1,64}", x) for x in codes - ) - ): - where[col.name] = sorted(x for x in codes if x is not None) + deny = reads.get(col.name, set()) - writes.get(col.name, set()) + if deny and all(re.fullmatch(r"[A-Za-z_]{1,64}", x) for x in deny): + where[col.name] = {"not_in": sorted(deny)} found = { "kind": "audit", "table": name, @@ -1247,12 +1249,16 @@ def _versions_frame( } table = v["table"] vpk = b.schema.tables[table].primary_key[0] - conds = " AND ".join( - f"{b.text('t', table, c)} = {b.literal(val)}" - if isinstance(val, str) - else f"{b.text('t', table, c)} IN ({', '.join(b.literal(x) for x in val)})" - for c, val in sorted(v["where"].items()) - ) + + def cond(c: str, val: Any) -> str: + if isinstance(val, str): + return f"{b.text('t', table, c)} = {b.literal(val)}" + # A deny-list of read codes: a NULL code still counts as a save. + ref = b.text("t", table, c) + listed = ", ".join(b.literal(x) for x in val["not_in"]) + return f"({ref} IS NULL OR {ref} NOT IN ({listed}))" + + conds = " AND ".join(cond(c, val) for c, val in sorted(v["where"].items())) sql = ( f"SELECT {b.text('t', table, v['record_column'])} AS {b.q('id')}, " f"{b.text('t', table, vpk)} AS {b.q('ver')}, " @@ -2200,11 +2206,20 @@ def _raw_effect( or (role.detail or {}).get("is_default") ): if role is not None and role.role in OPEN_ROLES: - # Set freely only when the person named it unchecked; else - # it must hold its default (the frame reads these rows from - # the table itself, so no computed pin applies). + # Set freely only when the person named it unchecked. Free + # text not named holds the empty value every demonstration + # stored (NULL or ''), matched on the row itself; anything + # else must hold its default. if c.name in settings.unchecked.get(table, set()): nrf.add(c.name) + elif role.role == "free": + empty = (role.detail or {}).get("empty") + if empty == "null": + m[c.name] = {"null": True} + nrf.add(c.name) + elif empty == "text": + m[c.name] = {"literal": ""} + nrf.add(c.name) continue if role is None or role.role in ("key", "link", "copied", "actor"): nrf.add(c.name) @@ -2504,62 +2519,76 @@ def check_read_only(connect: Any, dialect: str) -> list[str]: ): problems.append(f"the read account has {word}: {grant}") else: - cur.execute( - "SELECT rolsuper, rolcreaterole, rolcreatedb FROM pg_roles " - "WHERE rolname = current_user" + # Every role the account is, inherits, or can SET ROLE to (a + # NOINHERIT membership): what any of them can do, the account can. + roles = ( + "(SELECT oid, rolname, rolsuper, rolcreaterole, rolcreatedb " + "FROM pg_roles WHERE pg_has_role(current_user, oid, 'MEMBER')) r" ) - row = cur.fetchone() - if row and any(row): - problems.append( - "the read account is a superuser or can create roles or databases" - ) - # Effective privileges, so a role the account is a member of (or a - # predefined role such as pg_write_all_data) counts too, on every - # table, view and sequence of every schema the app can use. user_schemas = ( "n.nspname NOT IN ('pg_catalog', 'information_schema') " "AND n.nspname NOT LIKE 'pg\\_toast%' " "AND n.nspname NOT LIKE 'pg\\_temp%'" ) + + def who(role: str) -> str: + return "the read account" + ("" if role == user else f" (as {role})") + + cur.execute("SELECT current_user") + user = str(cur.fetchone()[0]) + cur.execute( + "SELECT r.rolname FROM " + roles + " WHERE r.rolsuper OR " + "r.rolcreaterole OR r.rolcreatedb ORDER BY 1" + ) + for (role,) in cur.fetchall(): + problems.append( + f"{who(role)} is a superuser or can create roles or databases" + ) cur.execute( - "SELECT n.nspname, c.relname, p.priv FROM pg_class c " - "JOIN pg_namespace n ON n.oid = c.relnamespace " + "SELECT r.rolname, n.nspname, c.relname, p.priv FROM " + roles + " " + "CROSS JOIN pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace " "CROSS JOIN unnest(ARRAY['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', " "'TRIGGER', 'REFERENCES']) AS p(priv) " "WHERE c.relkind IN ('r', 'p', 'v', 'm', 'f') AND " + user_schemas - + " AND has_table_privilege(c.oid, p.priv) " - "ORDER BY 1, 2, 3 LIMIT 20" + + " AND (has_table_privilege(r.oid, c.oid, p.priv) OR (p.priv IN " + "('INSERT', 'UPDATE', 'REFERENCES') AND " + "has_any_column_privilege(r.oid, c.oid, p.priv))) " + "ORDER BY 1, 2, 3, 4 LIMIT 20" ) - for schema_name, t, priv in cur.fetchall(): + for role, schema_name, t, priv in cur.fetchall(): problems.append( - f"the read account has {priv} on table {schema_name}.{t}" + f"{who(role)} has {priv} on table {schema_name}.{t} " + "(or on one of its columns)" ) cur.execute( - "SELECT n.nspname, c.relname, p.priv FROM pg_class c " - "JOIN pg_namespace n ON n.oid = c.relnamespace " + "SELECT r.rolname, n.nspname, c.relname, p.priv FROM " + roles + " " + "CROSS JOIN pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace " "CROSS JOIN unnest(ARRAY['USAGE', 'UPDATE']) AS p(priv) " "WHERE c.relkind = 'S' AND " + user_schemas - + " AND has_sequence_privilege(c.oid, p.priv) " - "ORDER BY 1, 2, 3 LIMIT 20" + + " AND has_sequence_privilege(r.oid, c.oid, p.priv) " + "ORDER BY 1, 2, 3, 4 LIMIT 20" ) - for schema_name, t, priv in cur.fetchall(): + for role, schema_name, t, priv in cur.fetchall(): problems.append( - f"the read account has {priv} on sequence {schema_name}.{t} " + f"{who(role)} has {priv} on sequence {schema_name}.{t} " "(it can move the counter)" ) cur.execute( - "SELECT n.nspname FROM pg_namespace n WHERE " + "SELECT r.rolname, n.nspname FROM " + roles + " " + "CROSS JOIN pg_namespace n WHERE " + user_schemas - + " AND has_schema_privilege(n.oid, 'CREATE') ORDER BY 1" + + " AND has_schema_privilege(r.oid, n.oid, 'CREATE') ORDER BY 1, 2" + ) + for role, schema_name in cur.fetchall(): + problems.append(f"{who(role)} has CREATE on schema {schema_name}") + cur.execute( + "SELECT r.rolname FROM " + roles + " WHERE " + "has_database_privilege(r.oid, current_database(), 'CREATE') ORDER BY 1" ) - for (schema_name,) in cur.fetchall(): - problems.append(f"the read account has CREATE on schema {schema_name}") - cur.execute("SELECT has_database_privilege(current_database(), 'CREATE')") - row = cur.fetchone() - if row and row[0]: - problems.append("the read account has CREATE on the database") + for (role,) in cur.fetchall(): + problems.append(f"{who(role)} has CREATE on the database") finally: conn.rollback() conn.close() @@ -2964,6 +2993,15 @@ def table_cols(key: str) -> dict[str, set[str]]: for own_cols in open_by_kind.values(): for t, roles_of in own_cols.items(): unchecked.setdefault(t, set()).update(roles_of) + proposed_open: dict[str, list[str]] = {} + for own_cols in open_by_kind.values(): + for t, roles_of in own_cols.items(): + for c, role_name in roles_of.items(): + if role_name != "free": + proposed_open.setdefault(t, []) + if c not in proposed_open[t]: + proposed_open[t].append(c) + proposed_open = {t: sorted(c) for t, c in sorted(proposed_open.items())} for t, cols in unchecked_named.items(): extra = cols - unchecked.get(t, set()) if extra: @@ -3073,13 +3111,11 @@ def table_cols(key: str) -> dict[str, set[str]]: "columns_remove and unchecked. An answer can name only what this " "question lists. Unanswered, nothing is excused, no table is a " "log and no column is left unchecked.", - # The proposal names every listed column, so accepting it is - # explicit; a person may leave any out to have it checked. - { - "accept": True, - "unchecked": {t: sorted(c) for t, c in sorted(unchecked.items())}, - } - if unchecked + # The proposal names the columns that vary or are time stamps, + # so accepting it is explicit. Free text every demonstration + # left empty stays pinned empty unless the person adds it. + {"accept": True, "unchecked": proposed_open} + if proposed_open else "accept", ) ) diff --git a/packages/flow/tests/test_generate_verify_contract_attacks_r2.py b/packages/flow/tests/test_generate_verify_contract_attacks_r2.py index 185077ef20..d51e4a5df6 100644 --- a/packages/flow/tests/test_generate_verify_contract_attacks_r2.py +++ b/packages/flow/tests/test_generate_verify_contract_attacks_r2.py @@ -188,9 +188,10 @@ def _fresh(app: AppV, subject: str) -> int: def test_versions_read_write_events_of_creates_and_updates(gv: dict[str, Any]) -> None: v = gv["report"]["kinds"]["edit_description"]["versions"] assert v["kind"] == "audit" and v["table"] == "audits", v - # Compared only with creates; a column that varies with the fields saved - # (field_name) is never a filter. - assert v["where"] == {"action": ["create", "update"]}, v + # No demonstration logged a code on a record it didn't change, so no + # code is shown to be a read: every entry counts as a save. A column + # that varies with the fields saved (field_name) is never a filter. + assert v["where"] == {}, v def test_correct_status_update_confirmed(gv: dict[str, Any]) -> None: diff --git a/packages/flow/tests/test_generate_verify_contract_attacks_r3.py b/packages/flow/tests/test_generate_verify_contract_attacks_r3.py new file mode 100644 index 0000000000..8d54af31f0 --- /dev/null +++ b/packages/flow/tests/test_generate_verify_contract_attacks_r3.py @@ -0,0 +1,320 @@ +# ruff: noqa: F811 (pytest fixtures imported from the generator test, then used as arguments) +"""The adversarial review's round-3 attacks on generated contracts (#1672). + +The round-2 tracker with no version column, so updates read their versions +from the audit trail, on a throwaway PostgreSQL. The person's answers are +the proposal plus ``logs_add: [audits]``. Every wrong write must stop; the +correct writes beside them must be CONFIRMED. Also: the PostgreSQL read-only +check counts roles reachable with SET ROLE and column grants. Records are +made up. +""" + +from __future__ import annotations + +import json +import os +from typing import Any + +import pytest +import yaml + +psycopg2 = pytest.importorskip("psycopg2") + +from openadapt_flow.generate import verify_contract as gen # noqa: E402 +from tests.test_generate_verify_contract import BOT, _run, pg # noqa: E402,F401 +from tests.test_generate_verify_contract_attacks_r2 import ( # noqa: E402 + KINDS_V, + SCHEMA_V, + AppV, +) + + +class AppA(AppV): + """Logs each save under the given event code (a bulk edit, say).""" + + def save( + self, iid: int, sets: dict[str, Any], *, user: int = BOT, action: str = "update" + ) -> None: + for col, val in sets.items(): + if col == "status": + col, val = ( + "status_id", + self.one("SELECT id FROM statuses WHERE name=%s", (val,)), + ) + old = self.one(f"SELECT {col}::text FROM issues WHERE id=%s", (iid,)) + self.q(f"UPDATE issues SET {col}=%s WHERE id=%s", (val, iid)) + self.q( + "INSERT INTO audits (issue_id, user_id, action, field_name, old_value, new_value) " + "VALUES (%s,%s,%s,%s,%s,%s)", + (iid, user, action, col, old, None if val is None else f"v{val}"), + ) + self.q("UPDATE issues SET updated_on=now() WHERE id=%s", (iid,)) + self.q("UPDATE projects SET updated_on=now() WHERE id=(SELECT project_id FROM issues WHERE id=%s)", + (iid,)) # fmt: skip + + +@pytest.fixture(scope="module") +def ga(pg: Any) -> dict[str, Any]: + root, port = pg["root"], pg["port"] + admin = psycopg2.connect( + host="127.0.0.1", port=port, user="admin", dbname="postgres" + ) + admin.autocommit = True + admin.cursor().execute("CREATE DATABASE tracker3") + admin.close() + app = AppA(port, "tracker3") + with app.con() as c, c.cursor() as cur: + cur.execute( + "DO $$BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='oa_reader') " + "THEN CREATE ROLE oa_reader LOGIN PASSWORD 'reader-pw'; END IF; END$$" + ) + cur.execute(SCHEMA_V) + os.environ["OA_GEN_TEST_PW"] = "reader-pw" + work = root / "work3" + work.mkdir() + read = {"sql_driver": "psycopg2", + "sql_connect_args": {"host": "127.0.0.1", "port": port, "user": "oa_reader", "dbname": "tracker3"}, + "sql_password_env": "OA_GEN_TEST_PW"} # fmt: skip + (work / "read.yaml").write_text(yaml.safe_dump(read)) + (work / "kinds.yaml").write_text(yaml.safe_dump(KINDS_V, allow_unicode=True)) + common = ["--read", str(work / "read.yaml")] + + def demo(kind: str, values: dict[str, Any], write: Any) -> None: + assert gen.main(["snapshot", *common, "--out", str(work / "before.json")]) == 0 + write() + assert gen.main(["record", *common, "--before", str(work / "before.json"), "--kind", kind, + "--values", json.dumps(values), "--demos", str(work / "demos")]) == 0 # fmt: skip + + for k in KINDS_V["kinds"]: + for ex in k["examples"]: + v = ex["values"] + if k["kind"] == "create_issue": + demo("create_issue", v, lambda v=v: app.create_issue(v)) + else: + sets = {p: v[p] for p in k["parameters"][2:] if v.get(p) is not None} + demo( + k["kind"], + v, + lambda v=v, s=sets: app.save( + app.find(v["project"], v["subject"]), s + ), + ) + argv = ["propose", *common, "--kinds", str(work / "kinds.yaml"), "--demos", str(work / "demos"), + "--out", str(work / "out"), "--actor-param", f"bot_user_id={BOT}"] # fmt: skip + assert gen.main(argv) == 0 + report = json.loads((work / "out" / "report.json").read_text()) + answers = {q["key"]: q["proposal"] for q in report["questions"]} + bk = answers["bookkeeping"] + bk = {"accept": True} if bk == "accept" else dict(bk) + bk["logs_add"] = ["audits"] + answers["bookkeeping"] = bk + (work / "answers.yaml").write_text(yaml.safe_dump(answers)) + assert gen.main([*argv, "--answers", str(work / "answers.yaml")]) == 0 + report = json.loads((work / "out" / "report.json").read_text()) + return {"app": app, "out": work / "out", "report": report, "read": read, + "answers": answers} # fmt: skip + + +def _fresh(app: AppA, subject: str) -> int: + return app.create_issue( + {"project": "Help Desk", "subject": subject, "status": "New"} + ) + + +# -- SF1: free text every demonstration left empty stays empty ------------------- + + +def test_proposal_leaves_free_text_out(ga: dict[str, Any]) -> None: + unchecked = ga["answers"]["bookkeeping"].get("unchecked", {}) + assert "description" not in unchecked.get("issues", []), unchecked + + +def test_correct_create_confirmed(ga: dict[str, Any]) -> None: + app = ga["app"] + v = { + "project": "Help Desk", + "subject": "P0 child", + "status": "New", + "parent": "VPN drops", + } + out = _run(ga, "create_issue", v, lambda: app.create_issue(v)) + assert out.startswith("CONFIRMED"), out + + +def test_create_that_also_fills_an_empty_description_stops(ga: dict[str, Any]) -> None: + app = ga["app"] + v = { + "project": "Help Desk", + "subject": "A1 child", + "status": "New", + "parent": "VPN drops", + } + + def write() -> None: + iid = app.create_issue(v) + app.q( + "UPDATE issues SET description='Refund approved, wire to acct 99' WHERE id=%s", + (iid,), + ) + + out = _run(ga, "create_issue", v, write) + assert not out.startswith("CONFIRMED"), out + + +# -- SF6.1: the audit table's own key never names the record --------------------- + + +def test_versions_come_from_the_audit_trail_for_every_update_kind( + ga: dict[str, Any], +) -> None: + for kind in ("set_status", "edit_description"): + v = ga["report"]["kinds"][kind]["versions"] + assert v and v["table"] == "audits" and v["record_column"] == "issue_id", ( + kind, + v, + ) + + +def test_lost_update_of_another_field_stops(ga: dict[str, Any]) -> None: + app = ga["app"] + iid = _fresh(app, "AS1") + v = {"project": "Help Desk", "subject": "AS1", "status": "Resolved"} + + def write() -> None: + app.save(iid, {"due_date": "2026-12-31"}, user=2) + app.save(iid, {"status": "Resolved", "due_date": None}) + + out = _run(ga, "set_status", v, write) + assert not out.startswith("CONFIRMED"), out + + +def test_lost_update_of_the_same_field_stops(ga: dict[str, Any]) -> None: + app = ga["app"] + iid = _fresh(app, "AS2") + v = {"project": "Help Desk", "subject": "AS2", "status": "Resolved"} + + def write() -> None: + app.save(iid, {"status": "In Progress"}, user=2) + app.save(iid, {"status": "Resolved"}) + + out = _run(ga, "set_status", v, write) + assert not out.startswith("CONFIRMED"), out + + +# -- SF3: an event code no demonstration showed counts as a save ----------------- + + +def test_no_code_is_shown_to_be_a_read_so_nothing_is_filtered( + ga: dict[str, Any], +) -> None: + v = ga["report"]["kinds"]["edit_description"]["versions"] + assert v["where"] == {}, v + + +def test_correct_description_edit_confirmed(ga: dict[str, Any]) -> None: + app = ga["app"] + iid = _fresh(app, "A2 ctrl") + v = {"project": "Help Desk", "subject": "A2 ctrl", "description": "Fixed"} + out = _run( + ga, "edit_description", v, lambda: app.save(iid, {"description": "Fixed"}) + ) + assert out.startswith("CONFIRMED"), out + + +def test_lost_update_under_an_unseen_code_stops(ga: dict[str, Any]) -> None: + app = ga["app"] + iid = _fresh(app, "A2C1") + v = {"project": "Help Desk", "subject": "A2C1", "description": "Fixed"} + + def write() -> None: + app.save(iid, {"due_date": "2026-12-31"}, user=2, action="bulk_update") + app.save(iid, {"description": "Fixed", "due_date": None}) + + out = _run(ga, "edit_description", v, write) + assert not out.startswith("CONFIRMED"), out + + +def test_same_field_lost_update_under_an_unseen_code_stops(ga: dict[str, Any]) -> None: + app = ga["app"] + iid = _fresh(app, "A2C2") + v = {"project": "Help Desk", "subject": "A2C2", "description": "Fixed"} + + def write() -> None: + app.save(iid, {"description": "Theirs"}, user=2, action="bulk_update") + app.save(iid, {"description": "Fixed"}) + + out = _run(ga, "edit_description", v, write) + assert not out.startswith("CONFIRMED"), out + + +def test_versions_filter_drops_only_a_code_shown_to_be_a_read( + ga: dict[str, Any], +) -> None: + """A read code is one the demonstrations logged on a record they didn't + change; the read keeps NULL codes and every other code.""" + frame = gen._versions_frame( + gen.SqlBuilder(_schema(ga)), + _plan_with_where({"action": {"not_in": ["read"]}}), + _settings(ga), + ) + assert frame is not None + sql = frame["read"]["sql_query"] + assert "IS NULL OR" in sql and "NOT IN ('read')" in sql, sql + + +def _schema(ga: dict[str, Any]) -> Any: + from openadapt_flow.generate import sql_store as store + + return store.read_schema(store.connector(ga["read"]), "postgresql") + + +def _plan_with_where(where: dict[str, Any]) -> Any: + plan = gen.KindPlan("edit_description") + plan.anchor = "issues" + plan.versions = {"kind": "audit", "table": "audits", "record_column": "issue_id", + "author_column": "user_id", "actor_param": "bot_user_id", "where": where} # fmt: skip + return plan + + +def _settings(ga: dict[str, Any]) -> Any: + return gen.Settings( + dict(ga["read"]), {"bot_user_id": str(BOT)}, 300, "test-environment" + ) + + +# -- SF4: PostgreSQL roles reachable with SET ROLE, and column grants ------------- + + +def test_pg_read_only_check_counts_set_role_and_column_grants(pg: Any) -> None: + port = pg["port"] + admin = psycopg2.connect( + host="127.0.0.1", port=port, user="admin", dbname="postgres" + ) + admin.autocommit = True + admin.cursor().execute("CREATE DATABASE rotest3") + admin.close() + a = psycopg2.connect(host="127.0.0.1", port=port, user="admin", dbname="rotest3") + a.autocommit = True + a.cursor().execute( + "REVOKE CREATE ON SCHEMA public FROM PUBLIC; CREATE TABLE t (id serial primary key, v text); " + "CREATE ROLE w3; GRANT INSERT, UPDATE ON t TO w3; " + "CREATE ROLE ro3 LOGIN NOINHERIT; GRANT SELECT ON t TO ro3; GRANT w3 TO ro3; " + "CREATE ROLE ro4 LOGIN; GRANT SELECT ON t TO ro4; GRANT UPDATE (v) ON t TO ro4; " + "CREATE ROLE ro6 LOGIN; GRANT SELECT ON t TO ro6; " + "REVOKE CREATE ON DATABASE rotest3 FROM PUBLIC; REVOKE TEMP ON DATABASE rotest3 FROM PUBLIC" + ) # fmt: skip + a.close() + + def check(role: str) -> list[str]: + return gen.check_read_only( + lambda: psycopg2.connect( + host="127.0.0.1", port=port, user=role, dbname="rotest3" + ), + "postgresql", + ) + + ro3 = check("ro3") + assert any("(as w3) has INSERT on table public.t" in p for p in ro3), ro3 + ro4 = check("ro4") + assert any("has UPDATE on table public.t" in p for p in ro4), ro4 + assert check("ro6") == [] From 19d74134a7a5dc2b77a1a14bf5f627a4c5288000 Mon Sep 17 00:00:00 2001 From: abrichr Date: Sat, 31 Oct 2026 02:35:23 -0400 Subject: [PATCH 93/94] Read codes: an audit row naming a record the demonstration added or changed in any table is a save --- .../flow/openadapt_flow/generate/verify_contract.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/flow/openadapt_flow/generate/verify_contract.py b/packages/flow/openadapt_flow/generate/verify_contract.py index bdd2f58bd4..2b472d69da 100644 --- a/packages/flow/openadapt_flow/generate/verify_contract.py +++ b/packages/flow/openadapt_flow/generate/verify_contract.py @@ -1184,14 +1184,17 @@ def _audit_source( # demonstration logged it on a record of this table it didn't add or # change, and no demonstration logged it on one it did. An unseen # code (a bulk edit, an import) or a NULL one counts as a save. - pk = t.primary_key[0] reads: dict[str, set[str]] = {} writes: dict[str, set[str]] = {} for d in [*create_demos, *update_demos]: + # Every record the demonstration added or changed, in any table: + # an audit row naming one of them is a save (a create logged on a + # contact, not a read of a lead). moved = { - str((ch.after or ch.before or {}).get(pk)) - for kind in ("added", "changed") - for ch in d.diff.of(plan.anchor, kind) + str((ch.after or {}).get(k)) + for ch in d.diff.changes + if ch.kind in ("added", "changed") and ch.table in schema.tables + for k in schema.tables[ch.table].primary_key } for ch in d.diff.of(name, "added"): r = ch.after or {} From b1ce2564e630827bc9eb9bd249bb5c8049be60d5 Mon Sep 17 00:00:00 2001 From: abrichr Date: Sat, 31 Oct 2026 02:53:26 -0400 Subject: [PATCH 94/94] CAPABILITIES: counts after the round-3 fixes --- CAPABILITIES.md | 2 +- llms-full.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CAPABILITIES.md b/CAPABILITIES.md index 78a8264316..f23ae086bf 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -147,7 +147,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 4, Redmine 2, EspoCRM 3, against 11 hand decisions each; every in-scope planted fault stopped (22/22, 19/19, 21/21); correct writes confirmed 40/43, 32/38, 27/40; evidence in the private study repository) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 4, Redmine 2, EspoCRM 3, against 11 hand decisions each; every in-scope planted fault stopped (22/22, 19/19, 21/21); correct writes confirmed 32/43, 32/38, 27/40; evidence in the private study repository) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](packages/flow/benchmark/fhir_medplum/public/RESULTS.md) | diff --git a/llms-full.txt b/llms-full.txt index 97a6ef51df..64d75e9fde 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -544,7 +544,7 @@ improvising or retrying when something is off. | Judge an "ensure the record exists" write (found, created once with every value, or needs review) through a FHIR server's read path only: searches, AuditEvent and `_history`, never the database; the coverage claim names that read path. An agent's own correction of the record it created counts as one write when the audit trail and history show only that agent, no other reader, and the same record in every version | On main ([#1640](https://github.com/OpenAdaptAI/OpenAdapt/pull/1640), corrections [#1655](https://github.com/OpenAdaptAI/OpenAdapt/pull/1655)) | Real app (Medplum 5.2.8; three blind fault suites: 17/18, 17/20, 19/20; no fault passed in the last two. Corrections, on 15 blind cases and the 37 public ones: false stops 8 to 2 and 9 to 4 for Claude, no harm passed that the old rule caught) | `openadapt_flow/ensure_record.py`, [ENSURE_RECORD.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/ENSURE_RECORD.md) | | Generate an ensure-record contract from three demonstrations, the server's CapabilityStatement and its SearchParameter definitions: field paths and `[system=…]` filters, comparison kinds, value maps, identity, search parameters, the closed record and the change feed. It refuses when unsure, asks a person to confirm an identity for a type with no reviewed template, and self-checks every demonstration plus a perturbation per field | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8: all 16 hand-written Patient lines regenerated; one blind write type, Immunization: 78 s of person time, 2 lines edited after the person declined the proposed identity; blind suite 23/24, no fault passed) | `openadapt_flow/generate/ensure_contract.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/contract_autogen/results/RESULTS.md) | | A stop card for a stopped write: the exact record difference and who else wrote, with Undo or Fix that reads back, or Open in the app; cost modelled from published operators and timed machine steps, as pre-registered | On main ([#1653](https://github.com/OpenAdaptAI/OpenAdapt/pull/1653)) | Real app (Medplum 5.2.8, blind suite: 16 stops, 14 one-tap with no failed read-back; modelled 42 s per caught harm against 420; one wrong category on a false stop, gone in the re-measure with `void`: 24/24, 15 stops, no wrong category, no failed read-back) | `benchmark/stop_card/`, [PREREG_REVIEW.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/stop_card/PREREG_REVIEW.md) | -| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 4, Redmine 2, EspoCRM 3, against 11 hand decisions each; every in-scope planted fault stopped (22/22, 19/19, 21/21); correct writes confirmed 40/43, 32/38, 27/40; evidence in the private study repository) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | +| Generate `openadapt.verify/v1` contracts from a MySQL, MariaDB or PostgreSQL schema, the kinds of write with their parameter names, and one demonstration write per example: one contract per field set (an update gets one for each set of fields it may change), list parameters by length, a `found` twin for a record already in place, the whole-database frame on every dialect, rows the app adds on its own declared by count, a recheck window the person confirms, and `frame.versions` (a lost update) from a version column or the app's audit trail of saves. Every undecided point is one numbered question; every contract is loaded and dry-read against the live store before it is written; a needed `TRIGGER` grant or view is applied with a flag or the kind is refused. Not yet: OpenAPI metadata | [#1672](https://github.com/OpenAdaptAI/OpenAdapt/pull/1672) | Real app (first study, instance copies; questions for a person: Dolibarr 4, Redmine 2, EspoCRM 3, against 11 hand decisions each; every in-scope planted fault stopped (22/22, 19/19, 21/21); correct writes confirmed 32/43, 32/38, 27/40; evidence in the private study repository) | `python -m openadapt_flow.generate.verify_contract`, [GENERATE_CONTRACTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/GENERATE_CONTRACTS.md) | | `void` in an ensure-record contract: values that make a record assert nothing (`status="entered-in-error"`). A record holding one is never the person, is not a duplicate, and a created one needs review; it names values to leave out, never values to keep, and can't name an identity field | On main ([#1657](https://github.com/OpenAdaptAI/OpenAdapt/pull/1657)) | Real app (Medplum 5.2.8: 8 live cases written in advance, 8/8 with the line, 5/8 and one silent pass without it; frozen blind Immunization suite 24/24); adversarial review recorded | `openadapt_flow/ensure_record.py`, [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/results/RESULTS.md), [REVIEW_VOID.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/void/REVIEW_VOID.md) | | Check a patient registration on a commercial EHR through its public FHIR sandbox: Epic (open.epic, R4), read through FHIR search and read with SMART backend auth; the frame is the search set, since the sandbox has no AuditEvent or `_history` | On main ([#1656](https://github.com/OpenAdaptAI/OpenAdapt/pull/1656)) | Real app (Epic public sandbox, August 2026 release; blind round 12/13 with none passed as verified, the miss a false refusal fixed in the contract; caught Epic dropping the phone on a 201 create) | `benchmark/commercial_ehr/`, [FINDINGS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/commercial_ehr/FINDINGS.md) | | A public benchmark a vendor reruns with their own agent: `openadapt bench medplum --agent ` runs 37 written-for-it cases (Patient, AllergyIntolerance, Appointment) against a local Medplum through a fault proxy, and scores the agent's claim, a read-back and OpenAdapt's verifier against an end-state oracle | On main ([#1652](https://github.com/OpenAdaptAI/OpenAdapt/pull/1652)) | Real app (Medplum 5.2.8; Claude Sonnet 5.5: OpenAdapt missed 1 of 9 harms with 4 false stops, all corrections that changed the record's identity, against 8 missed by the claim and the read-back; cold run from clone to scorecard 5 min) | [README](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/README.md), [RESULTS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/fhir_medplum/public/RESULTS.md) |