pythia-plsql 0.3.5__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pythia_plsql-0.3.5/scripts/pythia_plsql.egg-info → pythia_plsql-0.4.1}/PKG-INFO +8 -2
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/README.md +7 -1
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/pyproject.toml +1 -1
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/scripts/pythia.py +198 -6
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1/scripts/pythia_plsql.egg-info}/PKG-INFO +8 -2
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/skills/pythia-apply/SKILL.md +15 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/tests/test_phase3.py +81 -9
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/LICENSE +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/compile-errors.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/dependencies.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/impact.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/invalid-objects.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/name-occupants.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/object-source.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/plscope-enabled.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/plscope-statements.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/plscope-usages.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/session-privileges.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/similar-candidates.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/queries/source.sql +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/scripts/pythia_plsql.egg-info/SOURCES.txt +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/scripts/pythia_plsql.egg-info/dependency_links.txt +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/scripts/pythia_plsql.egg-info/entry_points.txt +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/scripts/pythia_plsql.egg-info/requires.txt +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/scripts/pythia_plsql.egg-info/top_level.txt +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/setup.cfg +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/skills/pythia-explore/SKILL.md +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/skills/pythia-explore/reference/data-dictionary.md +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/skills/pythia-impact/SKILL.md +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/skills/pythia-review/SKILL.md +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/skills/pythia-review/reference/antipatterns.md +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/skills/pythia-setup/SKILL.md +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/skills/pythia-skill-author/SKILL.md +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/skills/pythia-write/SKILL.md +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/skills/pythia-write/reference/patterns.md +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/tests/test_install.py +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/tests/test_phase1.py +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/tests/test_phase2.py +0 -0
- {pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/tests/test_phase5.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythia-plsql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/thaildhe172591/pythia
|
|
@@ -115,13 +115,19 @@ and WSL are all CI-tested.
|
|
|
115
115
|
| `src` source, compiler line numbers | `errors` compile errors, line:col | `policy` show · set |
|
|
116
116
|
| `args` signatures | `invalid` everything broken | `unistr` exact non-ASCII literals |
|
|
117
117
|
| `ddl` via DBMS_METADATA | `plscope` exact identifier usages | `agent-user` least-privilege setup |
|
|
118
|
-
| `cols` columns + types | `similar` programs named like this | |
|
|
118
|
+
| `cols` columns + types | `similar` programs named like this | `history` every captured version |
|
|
119
119
|
| `grep` search all source | | |
|
|
120
120
|
| `sql` free query (SELECT/WITH only) | | |
|
|
121
121
|
|
|
122
122
|
Every command takes `--json` (machine output), `--conn` (pick a connection), and
|
|
123
123
|
caps output with explicit truncation markers so context windows stay intact.
|
|
124
124
|
|
|
125
|
+
**The safety net covers hand edits too**: `src` and `impact` snapshot the
|
|
126
|
+
object silently into the journal, each with a runnable rollback file, so a
|
|
127
|
+
change made later in SQL Developer still has something to go back to —
|
|
128
|
+
`pythia history <OBJECT>` lists the versions. Source that moved with no
|
|
129
|
+
apply behind it is reported as drift.
|
|
130
|
+
|
|
125
131
|
**Your house style is config, not folklore**: put naming patterns in
|
|
126
132
|
`.pythia/conventions.json` and apply previews warn when a new object's name
|
|
127
133
|
drifts; put the prose rules in `.pythia/conventions.md` and the skills make
|
|
@@ -101,13 +101,19 @@ and WSL are all CI-tested.
|
|
|
101
101
|
| `src` source, compiler line numbers | `errors` compile errors, line:col | `policy` show · set |
|
|
102
102
|
| `args` signatures | `invalid` everything broken | `unistr` exact non-ASCII literals |
|
|
103
103
|
| `ddl` via DBMS_METADATA | `plscope` exact identifier usages | `agent-user` least-privilege setup |
|
|
104
|
-
| `cols` columns + types | `similar` programs named like this | |
|
|
104
|
+
| `cols` columns + types | `similar` programs named like this | `history` every captured version |
|
|
105
105
|
| `grep` search all source | | |
|
|
106
106
|
| `sql` free query (SELECT/WITH only) | | |
|
|
107
107
|
|
|
108
108
|
Every command takes `--json` (machine output), `--conn` (pick a connection), and
|
|
109
109
|
caps output with explicit truncation markers so context windows stay intact.
|
|
110
110
|
|
|
111
|
+
**The safety net covers hand edits too**: `src` and `impact` snapshot the
|
|
112
|
+
object silently into the journal, each with a runnable rollback file, so a
|
|
113
|
+
change made later in SQL Developer still has something to go back to —
|
|
114
|
+
`pythia history <OBJECT>` lists the versions. Source that moved with no
|
|
115
|
+
apply behind it is reported as drift.
|
|
116
|
+
|
|
111
117
|
**Your house style is config, not folklore**: put naming patterns in
|
|
112
118
|
`.pythia/conventions.json` and apply previews warn when a new object's name
|
|
113
119
|
drifts; put the prose rules in `.pythia/conventions.md` and the skills make
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pythia-plsql"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -42,6 +42,7 @@ you always know whether you saw everything.
|
|
|
42
42
|
pythia journal restore <id>
|
|
43
43
|
pythia policy
|
|
44
44
|
pythia install
|
|
45
|
+
pythia history MY_PACKAGE
|
|
45
46
|
pythia unistr "Nhóm không được để trống"
|
|
46
47
|
pythia agent-user --save
|
|
47
48
|
"""
|
|
@@ -631,6 +632,84 @@ def load_settings(root):
|
|
|
631
632
|
sys.exit(f"Cannot parse {path}: {e}")
|
|
632
633
|
|
|
633
634
|
|
|
635
|
+
def source_sha(text):
|
|
636
|
+
import hashlib
|
|
637
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def fetch_source(conn, schema, name):
|
|
641
|
+
"""Current source of an object, as ALL_SOURCE holds it. ('', None) when
|
|
642
|
+
the name is not a source object (a table, say)."""
|
|
643
|
+
_, rows = run_query(conn, load_query("source.sql"), {"s": schema, "n": name})
|
|
644
|
+
if not rows:
|
|
645
|
+
return "", None
|
|
646
|
+
return "".join(cell(r[2]) for r in rows), rows[0][0]
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def last_known_state(root, name):
|
|
650
|
+
"""What the database held, as far as the journal knows: the newest entry
|
|
651
|
+
for this object, and the source that entry says was really there.
|
|
652
|
+
A preview wrote nothing, so its `before` is the real state."""
|
|
653
|
+
for eid in list_journal_entries(root): # newest first
|
|
654
|
+
e = read_journal_entry(root, eid)
|
|
655
|
+
m = e["meta"]
|
|
656
|
+
if str(m.get("object", "")).upper() != name.upper():
|
|
657
|
+
continue
|
|
658
|
+
real = e["after"] if (m.get("applied") or m.get("snapshot")) else e["before"]
|
|
659
|
+
return real, eid, m
|
|
660
|
+
return None, None, None
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def auto_snapshot(conn, schema, name, ns):
|
|
664
|
+
"""Capture an object's source the moment an agent focuses on it, so a
|
|
665
|
+
later hand-edit outside pythia still has something to go back to.
|
|
666
|
+
|
|
667
|
+
Silent by design: writing to the journal costs the agent no context, and
|
|
668
|
+
a line of "snapshot taken" on every read would cost it on every read.
|
|
669
|
+
The one thing worth saying is drift — the source moved without an apply
|
|
670
|
+
of ours explaining it — and that goes to stderr, so --json stays clean.
|
|
671
|
+
|
|
672
|
+
Returns a drift warning, or None.
|
|
673
|
+
"""
|
|
674
|
+
root = getattr(ns, "project_root", None)
|
|
675
|
+
if root is None or not load_settings(root).get("auto_snapshot", True):
|
|
676
|
+
return None
|
|
677
|
+
try:
|
|
678
|
+
text, otype = fetch_source(conn, schema, name)
|
|
679
|
+
if not text.strip():
|
|
680
|
+
return None # not a source object; nothing to keep
|
|
681
|
+
known, eid, meta = last_known_state(root, name)
|
|
682
|
+
if known is not None and source_sha(known) == source_sha(text):
|
|
683
|
+
return None # unchanged — no entry, no output
|
|
684
|
+
write_journal_entry(root, otype or "OBJECT", name.upper(), text, text,
|
|
685
|
+
{"schema": schema, "connection": ns.conn_name,
|
|
686
|
+
"snapshot": True, "applied": False,
|
|
687
|
+
"sha": source_sha(text)})
|
|
688
|
+
if known is None:
|
|
689
|
+
return None # first sighting is a baseline
|
|
690
|
+
rollback = journal_root(root) / eid / "restore.sql"
|
|
691
|
+
return (f"! {name.upper()} changed outside pythia since {eid}.\n"
|
|
692
|
+
f" Rollback file for the previous version: {rollback}\n"
|
|
693
|
+
f" All versions: {invocation()} history {name.upper()}")
|
|
694
|
+
except Exception: # noqa: BLE001
|
|
695
|
+
return None # a safety net must never break a read
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def report_drift(msg):
|
|
699
|
+
if msg:
|
|
700
|
+
print(paint(msg, "yellow", color_enabled(sys.stderr)), file=sys.stderr)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def journaled_objects(root):
|
|
704
|
+
names = {}
|
|
705
|
+
for eid in list_journal_entries(root): # newest first
|
|
706
|
+
m = read_journal_entry(root, eid)["meta"]
|
|
707
|
+
n = str(m.get("object", "")).upper()
|
|
708
|
+
if n and n != "STATEMENT" and n not in names:
|
|
709
|
+
names[n] = eid[:19] # the entry's timestamp
|
|
710
|
+
return names
|
|
711
|
+
|
|
712
|
+
|
|
634
713
|
def undo_group_action(ns):
|
|
635
714
|
"""Undoing a CREATE is a DROP, which is `structural` — so the policy on
|
|
636
715
|
that group decides whether the restore command we print can run."""
|
|
@@ -827,6 +906,43 @@ def cmd_check(conn, schema, ns):
|
|
|
827
906
|
if warn:
|
|
828
907
|
print("\n" + paint(warn, "yellow", color_enabled(sys.stderr)),
|
|
829
908
|
file=sys.stderr)
|
|
909
|
+
report_drift(drift_summary(conn, schema, ns))
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def drift_summary(conn, schema, ns):
|
|
913
|
+
"""One line, only when objects pythia knows have moved since it last saw
|
|
914
|
+
them. Uses LAST_DDL_TIME — a single query for every journaled object,
|
|
915
|
+
instead of re-reading every source.
|
|
916
|
+
ponytail: LAST_DDL_TIME also ticks on a bare recompile, so this is a
|
|
917
|
+
'go look' signal; `history` and the src/impact hash comparison are the
|
|
918
|
+
precise ones."""
|
|
919
|
+
root = getattr(ns, "project_root", None)
|
|
920
|
+
if root is None:
|
|
921
|
+
return None
|
|
922
|
+
try:
|
|
923
|
+
known = journaled_objects(root)
|
|
924
|
+
if not known:
|
|
925
|
+
return None
|
|
926
|
+
names = sorted(known)[:200] # one query, bounded
|
|
927
|
+
binds = {"s": schema}
|
|
928
|
+
placeholders = []
|
|
929
|
+
for i, n in enumerate(names):
|
|
930
|
+
binds[f"n{i}"] = n
|
|
931
|
+
placeholders.append(f":n{i}")
|
|
932
|
+
_, rows = run_query(
|
|
933
|
+
conn, "select object_name, to_char(last_ddl_time,"
|
|
934
|
+
"'yyyy-mm-dd\"T\"hh24-mi-ss') from all_objects "
|
|
935
|
+
f"where owner = :s and object_name in ({','.join(placeholders)})",
|
|
936
|
+
binds)
|
|
937
|
+
moved = [r[0] for r in rows if str(r[1]) > known.get(str(r[0]), "")]
|
|
938
|
+
if not moved:
|
|
939
|
+
return None
|
|
940
|
+
shown = ", ".join(moved[:3]) + ("…" if len(moved) > 3 else "")
|
|
941
|
+
return (f"! {len(moved)} object(s) changed since pythia last saw them: "
|
|
942
|
+
f"{shown}\n Versions and restores: {invocation()} history "
|
|
943
|
+
f"<NAME>")
|
|
944
|
+
except Exception: # noqa: BLE001
|
|
945
|
+
return None
|
|
830
946
|
|
|
831
947
|
|
|
832
948
|
def cmd_ls(conn, schema, ns):
|
|
@@ -846,8 +962,10 @@ def cmd_src(conn, schema, ns):
|
|
|
846
962
|
if not rows:
|
|
847
963
|
sys.exit(f"No source found for {ns.name!r} in schema {schema}.")
|
|
848
964
|
total = len(rows)
|
|
965
|
+
drift = auto_snapshot(conn, schema, ns.name, ns)
|
|
849
966
|
shown, truncated = clip(rows, ns.max_lines, ns.offset)
|
|
850
967
|
emit_source(ns, shown, truncated, total)
|
|
968
|
+
report_drift(drift)
|
|
851
969
|
|
|
852
970
|
|
|
853
971
|
def cmd_args(conn, schema, ns):
|
|
@@ -955,19 +1073,26 @@ def cmd_deps(conn, schema, ns):
|
|
|
955
1073
|
def cmd_impact(conn, schema, ns):
|
|
956
1074
|
cols, rows = run_query(conn, load_query("impact.sql"),
|
|
957
1075
|
{"s": schema, "n": ns.name, "depth": ns.depth})
|
|
1076
|
+
# impact is mandatory before any change, so it is the surest "about to
|
|
1077
|
+
# touch this" signal there is — the best possible place to capture a
|
|
1078
|
+
# before-state the developer never had to ask for
|
|
1079
|
+
drift = auto_snapshot(conn, schema, ns.name, ns)
|
|
958
1080
|
shown, truncated = clip(rows, ns.limit)
|
|
959
1081
|
if ns.json:
|
|
960
1082
|
print(json_envelope(ns.command, ns.conn_name, ns.schema, cols, shown, truncated,
|
|
961
1083
|
summary=impact_summary(rows)))
|
|
1084
|
+
report_drift(drift)
|
|
962
1085
|
return
|
|
963
1086
|
if not rows:
|
|
964
1087
|
print(f"-- nothing depends on {ns.name.upper()} "
|
|
965
1088
|
f"(within {schema}, depth {ns.depth})")
|
|
1089
|
+
report_drift(drift)
|
|
966
1090
|
return
|
|
967
1091
|
sys.stdout.write(render_tree(shown, f"{schema}.{ns.name.upper()}"))
|
|
968
1092
|
if truncated:
|
|
969
1093
|
print(f"-- truncated at {len(shown)} rows (raise --limit, or --limit 0 for no cap)")
|
|
970
1094
|
print(impact_summary(rows))
|
|
1095
|
+
report_drift(drift)
|
|
971
1096
|
|
|
972
1097
|
|
|
973
1098
|
def cmd_similar(conn, schema, ns):
|
|
@@ -1212,6 +1337,11 @@ def run_apply(conn, schema, ns, file_text, origin=None):
|
|
|
1212
1337
|
print(f" {paint_diff_line(ln, en)}")
|
|
1213
1338
|
print(paint(f"\n Snapshot saved: {journal_root(ns.project_root) / entry}",
|
|
1214
1339
|
"dim", en))
|
|
1340
|
+
if not confirmed:
|
|
1341
|
+
print(paint(" Rollback file for the current database version — "
|
|
1342
|
+
"use it if this is\n run by hand instead: "
|
|
1343
|
+
f"{journal_root(ns.project_root) / entry / 'restore.sql'}",
|
|
1344
|
+
"dim", en))
|
|
1215
1345
|
if not confirmed:
|
|
1216
1346
|
print(f"\n To apply:\n "
|
|
1217
1347
|
+ paint(f"{invocation()} apply {ns.file} --confirm {token}",
|
|
@@ -1351,6 +1481,48 @@ def cmd_policy(conn, schema, ns):
|
|
|
1351
1481
|
print(ROLLBACK_TABLE)
|
|
1352
1482
|
|
|
1353
1483
|
|
|
1484
|
+
def cmd_history(conn, schema, ns):
|
|
1485
|
+
"""Every version of one object the journal holds, newest first. Compact
|
|
1486
|
+
on purpose — an agent reads this to choose an id, not to read source."""
|
|
1487
|
+
root = ns.project_root
|
|
1488
|
+
name = ns.name.upper()
|
|
1489
|
+
rows = []
|
|
1490
|
+
for eid in list_journal_entries(root):
|
|
1491
|
+
e = read_journal_entry(root, eid)
|
|
1492
|
+
m = e["meta"]
|
|
1493
|
+
if str(m.get("object", "")).upper() != name:
|
|
1494
|
+
continue
|
|
1495
|
+
real = e["after"] if (m.get("applied") or m.get("snapshot")) else e["before"]
|
|
1496
|
+
kind = ("applied" if m.get("applied") else
|
|
1497
|
+
"snapshot" if m.get("snapshot") else "preview")
|
|
1498
|
+
rows.append({"entry": eid, "kind": kind,
|
|
1499
|
+
"lines": len(real.splitlines()),
|
|
1500
|
+
"sha": source_sha(real),
|
|
1501
|
+
"rollback_file": str(journal_root(root) / eid
|
|
1502
|
+
/ "restore.sql")})
|
|
1503
|
+
if ns.json:
|
|
1504
|
+
print(json.dumps({"object": name, "versions": rows}))
|
|
1505
|
+
return
|
|
1506
|
+
if not rows:
|
|
1507
|
+
print(f"-- no journal history for {name}. It is captured the first "
|
|
1508
|
+
f"time you run `{invocation()} src {name}` or "
|
|
1509
|
+
f"`{invocation()} impact {name}`.")
|
|
1510
|
+
return
|
|
1511
|
+
prev = None
|
|
1512
|
+
for r in rows: # newest first; compare to the older one
|
|
1513
|
+
older = rows[rows.index(r) + 1] if rows.index(r) + 1 < len(rows) else None
|
|
1514
|
+
delta = ""
|
|
1515
|
+
if older and older["sha"] != r["sha"]:
|
|
1516
|
+
n = r["lines"] - older["lines"]
|
|
1517
|
+
delta = f" {n:+d} lines" if n else " content changed"
|
|
1518
|
+
print(f" {r['entry']:<44} {r['kind']:<9} {r['lines']:>5} lines{delta}")
|
|
1519
|
+
prev = r
|
|
1520
|
+
print(f"\n-- every version above has a ready-to-run rollback file:")
|
|
1521
|
+
print(f" {journal_root(root)}\\<entry>\\restore.sql")
|
|
1522
|
+
print(f"-- through pythia (previews first, you approve):")
|
|
1523
|
+
print(f" {invocation()} journal restore <entry>")
|
|
1524
|
+
|
|
1525
|
+
|
|
1354
1526
|
def cmd_journal(conn, schema, ns):
|
|
1355
1527
|
root = ns.project_root
|
|
1356
1528
|
if ns.action == "list":
|
|
@@ -1369,15 +1541,29 @@ def cmd_journal(conn, schema, ns):
|
|
|
1369
1541
|
if ns.action == "prune":
|
|
1370
1542
|
import shutil
|
|
1371
1543
|
removed = 0
|
|
1372
|
-
|
|
1373
|
-
|
|
1544
|
+
# A preview wrote nothing to the database, but its restore.sql holds
|
|
1545
|
+
# the version that was live at the time — the only rollback there is
|
|
1546
|
+
# for a change the developer then ran by hand. So drop a preview only
|
|
1547
|
+
# when its rollback is byte-identical to one a newer entry already
|
|
1548
|
+
# keeps: redundant captures go, unique ones never do.
|
|
1549
|
+
kept = {}
|
|
1550
|
+
for eid in list_journal_entries(root): # newest first
|
|
1551
|
+
e = read_journal_entry(root, eid)
|
|
1552
|
+
meta, obj = e["meta"], str(e["meta"].get("object", ""))
|
|
1553
|
+
if meta.get("applied") or meta.get("snapshot"):
|
|
1554
|
+
kept.setdefault(obj, set()).add(e["restore"])
|
|
1555
|
+
continue
|
|
1556
|
+
if e["restore"] in kept.get(obj, set()):
|
|
1374
1557
|
shutil.rmtree(journal_root(root) / eid)
|
|
1375
1558
|
removed += 1
|
|
1559
|
+
else:
|
|
1560
|
+
kept.setdefault(obj, set()).add(e["restore"])
|
|
1376
1561
|
if ns.json:
|
|
1377
1562
|
print(json.dumps({"pruned": removed}))
|
|
1378
1563
|
else:
|
|
1379
|
-
print(f"-- pruned {removed}
|
|
1380
|
-
"
|
|
1564
|
+
print(f"-- pruned {removed} redundant previews; applied entries, "
|
|
1565
|
+
"snapshots, and any preview holding a rollback nothing "
|
|
1566
|
+
"else has are all kept")
|
|
1381
1567
|
return
|
|
1382
1568
|
if not ns.id:
|
|
1383
1569
|
sys.exit(f"Usage: {invocation()} journal "
|
|
@@ -1752,9 +1938,11 @@ COMMANDS = {"check": cmd_check, "ls": cmd_ls, "src": cmd_src, "args": cmd_args,
|
|
|
1752
1938
|
"impact": cmd_impact, "similar": cmd_similar, "plscope": cmd_plscope,
|
|
1753
1939
|
"policy": cmd_policy, "journal": cmd_journal, "apply": cmd_apply,
|
|
1754
1940
|
"conventions": cmd_conventions, "install": cmd_install,
|
|
1755
|
-
"unistr": cmd_unistr, "agent-user": cmd_agent_user
|
|
1941
|
+
"unistr": cmd_unistr, "agent-user": cmd_agent_user,
|
|
1942
|
+
"history": cmd_history}
|
|
1756
1943
|
|
|
1757
|
-
NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr"
|
|
1944
|
+
NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr",
|
|
1945
|
+
"history"}
|
|
1758
1946
|
|
|
1759
1947
|
|
|
1760
1948
|
# --- CLI ---------------------------------------------------------------------
|
|
@@ -1860,6 +2048,10 @@ def build_parser():
|
|
|
1860
2048
|
s.add_argument("--save", action="store_true",
|
|
1861
2049
|
help="add the new credential as <conn>_agent and make it "
|
|
1862
2050
|
"the default connection")
|
|
2051
|
+
s = sub.add_parser("history", parents=[common()],
|
|
2052
|
+
help="every captured version of one object, newest "
|
|
2053
|
+
"first — pick one to restore")
|
|
2054
|
+
s.add_argument("name")
|
|
1863
2055
|
s = sub.add_parser("unistr", parents=[common()],
|
|
1864
2056
|
help="Oracle unistr('...') literal for non-ASCII text "
|
|
1865
2057
|
"(Vietnamese messages stay exact)")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythia-plsql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/thaildhe172591/pythia
|
|
@@ -115,13 +115,19 @@ and WSL are all CI-tested.
|
|
|
115
115
|
| `src` source, compiler line numbers | `errors` compile errors, line:col | `policy` show · set |
|
|
116
116
|
| `args` signatures | `invalid` everything broken | `unistr` exact non-ASCII literals |
|
|
117
117
|
| `ddl` via DBMS_METADATA | `plscope` exact identifier usages | `agent-user` least-privilege setup |
|
|
118
|
-
| `cols` columns + types | `similar` programs named like this | |
|
|
118
|
+
| `cols` columns + types | `similar` programs named like this | `history` every captured version |
|
|
119
119
|
| `grep` search all source | | |
|
|
120
120
|
| `sql` free query (SELECT/WITH only) | | |
|
|
121
121
|
|
|
122
122
|
Every command takes `--json` (machine output), `--conn` (pick a connection), and
|
|
123
123
|
caps output with explicit truncation markers so context windows stay intact.
|
|
124
124
|
|
|
125
|
+
**The safety net covers hand edits too**: `src` and `impact` snapshot the
|
|
126
|
+
object silently into the journal, each with a runnable rollback file, so a
|
|
127
|
+
change made later in SQL Developer still has something to go back to —
|
|
128
|
+
`pythia history <OBJECT>` lists the versions. Source that moved with no
|
|
129
|
+
apply behind it is reported as drift.
|
|
130
|
+
|
|
125
131
|
**Your house style is config, not folklore**: put naming patterns in
|
|
126
132
|
`.pythia/conventions.json` and apply previews warn when a new object's name
|
|
127
133
|
drifts; put the prose rules in `.pythia/conventions.md` and the skills make
|
|
@@ -31,6 +31,21 @@ Three moves are FORBIDDEN for agents, and the CLI enforces the first two
|
|
|
31
31
|
your turn; the token is only used after the developer's approval
|
|
32
32
|
arrives as a NEW message.
|
|
33
33
|
|
|
34
|
+
## When the change will NOT go through apply
|
|
35
|
+
|
|
36
|
+
Sometimes the developer runs the file themselves — a DBA executes it, a
|
|
37
|
+
release process owns it, or policy denies the group. The preview still ran,
|
|
38
|
+
so a rollback file for the version currently live already exists. Hand it
|
|
39
|
+
over in the same message as the .sql file, by path:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
.pythia/journal/<entry>/restore.sql
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Never let a change leave your hands for manual execution without naming
|
|
46
|
+
that file. `pythia history <OBJECT>` lists every captured version if an
|
|
47
|
+
older one is wanted.
|
|
48
|
+
|
|
34
49
|
## The Workflow
|
|
35
50
|
|
|
36
51
|
**Before step 1:** if this conversation has not yet seen a standalone impact
|
|
@@ -540,25 +540,42 @@ def test_privilege_warning_speaks_proxy():
|
|
|
540
540
|
FakeConn(direct), "APP", "APP")
|
|
541
541
|
|
|
542
542
|
|
|
543
|
-
def
|
|
543
|
+
def test_prune_drops_only_redundant_previews():
|
|
544
|
+
"""A preview wrote nothing, but its restore.sql is the live version at
|
|
545
|
+
that moment — the only rollback for a change run by hand. Prune may drop
|
|
546
|
+
it only when a newer entry already keeps the identical rollback."""
|
|
544
547
|
import argparse
|
|
548
|
+
import contextlib
|
|
545
549
|
import datetime
|
|
550
|
+
import io
|
|
546
551
|
with tempfile.TemporaryDirectory() as td:
|
|
547
|
-
base = datetime.datetime(2026, 8,
|
|
548
|
-
|
|
552
|
+
base = datetime.datetime(2026, 8, 27, 9, 0, 0)
|
|
553
|
+
# two previews of the same object, same live version -> one is spare
|
|
554
|
+
pythia.write_journal_entry(td, "PROCEDURE", "P_A", "old", "new1",
|
|
549
555
|
{"applied": False}, now=base)
|
|
550
|
-
|
|
551
|
-
td, "PROCEDURE", "
|
|
556
|
+
keep_dup = pythia.write_journal_entry(
|
|
557
|
+
td, "PROCEDURE", "P_A", "old", "new2", {"applied": False},
|
|
552
558
|
now=base.replace(minute=1))
|
|
559
|
+
# a preview of another object, unique rollback -> must survive
|
|
560
|
+
lone = pythia.write_journal_entry(
|
|
561
|
+
td, "PROCEDURE", "P_B", "only_copy", "x", {"applied": False},
|
|
562
|
+
now=base.replace(minute=2))
|
|
563
|
+
applied = pythia.write_journal_entry(
|
|
564
|
+
td, "PROCEDURE", "P_C", "a", "b", {"applied": True},
|
|
565
|
+
now=base.replace(minute=3))
|
|
566
|
+
snap = pythia.write_journal_entry(
|
|
567
|
+
td, "PROCEDURE", "P_D", "a", "a",
|
|
568
|
+
{"snapshot": True, "applied": False}, now=base.replace(minute=4))
|
|
553
569
|
ns = argparse.Namespace(action="prune", project_root=td, json=False,
|
|
554
570
|
id=None)
|
|
555
|
-
import contextlib
|
|
556
|
-
import io
|
|
557
571
|
buf = io.StringIO()
|
|
558
572
|
with contextlib.redirect_stdout(buf):
|
|
559
573
|
pythia.cmd_journal(None, None, ns)
|
|
560
|
-
|
|
561
|
-
assert
|
|
574
|
+
left = pythia.list_journal_entries(td)
|
|
575
|
+
assert "pruned 1" in buf.getvalue(), buf.getvalue()
|
|
576
|
+
assert applied in left and snap in left # never touched
|
|
577
|
+
assert lone in left # unique rollback survives
|
|
578
|
+
assert keep_dup in left # newest of the pair kept
|
|
562
579
|
|
|
563
580
|
|
|
564
581
|
def test_agent_user_alter_form_and_warning_prediction():
|
|
@@ -687,6 +704,61 @@ def test_report_admits_a_created_objects_undo_is_blocked():
|
|
|
687
704
|
assert "will be refused" not in buf.getvalue().lower()
|
|
688
705
|
|
|
689
706
|
|
|
707
|
+
def test_auto_snapshot_captures_silently_and_writes_a_rollback_file():
|
|
708
|
+
"""Every captured version leaves a runnable rollback file — that is the
|
|
709
|
+
whole point for work done by hand, outside pythia. And capturing costs
|
|
710
|
+
the agent no context: nothing is printed when nothing moved."""
|
|
711
|
+
import argparse
|
|
712
|
+
import contextlib
|
|
713
|
+
import io
|
|
714
|
+
src = "PROCEDURE P_X AS\nBEGIN\n NULL;\nEND;\n"
|
|
715
|
+
script = {"from all_source": ([("TYPE", "LINE", "TEXT")],
|
|
716
|
+
[("PROCEDURE", i + 1, ln + "\n")
|
|
717
|
+
for i, ln in enumerate(src.splitlines())])}
|
|
718
|
+
with tempfile.TemporaryDirectory() as td:
|
|
719
|
+
ns = argparse.Namespace(project_root=td, conn_name="DEV")
|
|
720
|
+
buf = io.StringIO()
|
|
721
|
+
with contextlib.redirect_stdout(buf):
|
|
722
|
+
drift = pythia.auto_snapshot(FakeConn(script), "APP", "P_X", ns)
|
|
723
|
+
assert drift is None # first sighting: a baseline
|
|
724
|
+
assert buf.getvalue() == "" # and not one token of output
|
|
725
|
+
ids = pythia.list_journal_entries(td)
|
|
726
|
+
assert len(ids) == 1
|
|
727
|
+
e = pythia.read_journal_entry(td, ids[0])
|
|
728
|
+
assert e["meta"]["snapshot"] is True
|
|
729
|
+
rollback = e["restore"]
|
|
730
|
+
assert rollback.startswith("CREATE OR REPLACE PROCEDURE P_X")
|
|
731
|
+
assert (pythia.journal_root(td) / ids[0] / "restore.sql").is_file()
|
|
732
|
+
|
|
733
|
+
# reading it again unchanged must not create a second entry
|
|
734
|
+
assert pythia.auto_snapshot(FakeConn(script), "APP", "P_X", ns) is None
|
|
735
|
+
assert pythia.list_journal_entries(td) == ids
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def test_auto_snapshot_reports_drift_and_keeps_the_old_rollback():
|
|
739
|
+
"""Source moved with no apply of ours behind it — say so once, and point
|
|
740
|
+
at the rollback file for the version that was there before."""
|
|
741
|
+
import argparse
|
|
742
|
+
old_src = "PROCEDURE P_X AS\nBEGIN\n NULL;\nEND;\n"
|
|
743
|
+
new_src = "PROCEDURE P_X AS\nBEGIN\n other_thing;\nEND;\n"
|
|
744
|
+
|
|
745
|
+
def script(text):
|
|
746
|
+
return {"from all_source": ([("TYPE", "LINE", "TEXT")],
|
|
747
|
+
[("PROCEDURE", i + 1, ln + "\n")
|
|
748
|
+
for i, ln in enumerate(text.splitlines())])}
|
|
749
|
+
with tempfile.TemporaryDirectory() as td:
|
|
750
|
+
ns = argparse.Namespace(project_root=td, conn_name="DEV")
|
|
751
|
+
pythia.auto_snapshot(FakeConn(script(old_src)), "APP", "P_X", ns)
|
|
752
|
+
first = pythia.list_journal_entries(td)[0]
|
|
753
|
+
drift = pythia.auto_snapshot(FakeConn(script(new_src)), "APP", "P_X", ns)
|
|
754
|
+
assert drift and "changed outside pythia" in drift
|
|
755
|
+
assert "restore.sql" in drift
|
|
756
|
+
# the older version's rollback is still on disk and still runnable
|
|
757
|
+
old_rollback = pythia.read_journal_entry(td, first)["restore"]
|
|
758
|
+
assert "NULL;" in old_rollback
|
|
759
|
+
assert len(pythia.list_journal_entries(td)) == 2
|
|
760
|
+
|
|
761
|
+
|
|
690
762
|
def main():
|
|
691
763
|
failed = 0
|
|
692
764
|
for name, fn in sorted(globals().items()):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/scripts/pythia_plsql.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythia_plsql-0.3.5 → pythia_plsql-0.4.1}/skills/pythia-explore/reference/data-dictionary.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|