@dforge-core/dforge-mcp 0.2.46-next → 0.2.48
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.
- package/CHANGELOG.md +68 -1
- package/dist/server.js +71 -3
- package/package.json +47 -48
- package/resources/docs/dsl-reference.md +1 -1
- package/resources/schemas/data-views.schema.json +50 -0
- package/skills/dforge-1c-import/SKILL.md +3 -2
- package/skills/dforge-1c-import/references/environment.md +2 -1
- package/skills/dforge-1c-import/references/loading.md +5 -2
- package/skills/dforge-1c-import/references/measurements.md +1 -1
- package/skills/dforge-1c-import/scripts/README.md +88 -7
- package/skills/dforge-1c-import/scripts/lib/__pycache__/attrs.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/delta.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/meta.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/migrate.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/paths.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/posted.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/posting.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/seedstate.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/store.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/totals.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/transactions.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/attrs.py +2 -2
- package/skills/dforge-1c-import/scripts/lib/changes.py +326 -0
- package/skills/dforge-1c-import/scripts/lib/delta.py +109 -52
- package/skills/dforge-1c-import/scripts/lib/docusage.py +5 -4
- package/skills/dforge-1c-import/scripts/lib/dump1c.py +249 -92
- package/skills/dforge-1c-import/scripts/lib/external.py +4 -3
- package/skills/dforge-1c-import/scripts/lib/meta.py +12 -2
- package/skills/dforge-1c-import/scripts/lib/migrate.py +93 -74
- package/skills/dforge-1c-import/scripts/lib/onecd.py +269 -0
- package/skills/dforge-1c-import/scripts/lib/paths.py +21 -0
- package/skills/dforge-1c-import/scripts/lib/peek.py +3 -5
- package/skills/dforge-1c-import/scripts/lib/recorded.py +11 -11
- package/skills/dforge-1c-import/scripts/lib/recorders.py +9 -12
- package/skills/dforge-1c-import/scripts/lib/seedstate.py +21 -16
- package/skills/dforge-1c-import/scripts/lib/source_file.py +227 -0
- package/skills/dforge-1c-import/scripts/lib/source_mssql.py +158 -0
- package/skills/dforge-1c-import/scripts/lib/store.py +126 -0
- package/skills/dforge-1c-import/scripts/lib/totals.py +2 -4
- package/skills/dforge-1c-import/scripts/lib/transactions.py +28 -0
- package/skills/dforge-1c-import/scripts/project-template/.env.example +24 -0
- package/skills/dforge-1c-import/scripts/project-template/sources.py +10 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +49 -46
- package/skills/dforge-mcp-author/references/data-views.md +38 -2
- package/skills/dforge-mcp-author/references/formulas.md +38 -6
- package/skills/dforge-mcp-author/references/jobs.md +1 -1
- package/skills/dforge-mcp-author/references/print-templates.md +30 -5
- package/skills/dforge-mcp-author/references/queries.md +17 -2
- package/skills/dforge-mcp-author/references/registers.md +3 -0
- package/skills/dforge-mcp-author/references/reports.md +1 -1
- package/skills/dforge-mcp-author/references/security.md +2 -0
- package/skills/dforge-mcp-author/references/validation-checklist.md +7 -8
|
@@ -9,6 +9,13 @@ Bring a loaded tenant up to date with what 1C changed since the load: the cutove
|
|
|
9
9
|
python tools/1c.py delta --baseline [module ...] # mark a tenant loaded before marks existed
|
|
10
10
|
python tools/1c.py delta --since 2026-09-17 # dry run as if loaded on that day
|
|
11
11
|
|
|
12
|
+
It reads the export, not 1C: bring it up to date with `dump1c data --changes` first (minutes,
|
|
13
|
+
where a full `dump1c data` is an hour and more), and the delta is what changed between
|
|
14
|
+
the rows the tenant was loaded from and the rows that export holds. Only an export of a
|
|
15
|
+
server infobase can carry one - a file infobase's `_Version` is a per-table counter, not a
|
|
16
|
+
database-wide rowversion, so a file export has no horizon to read changes against, and a
|
|
17
|
+
file infobase is small enough to reload whole (`migrate run --truncate`).
|
|
18
|
+
|
|
12
19
|
What 1C gives to detect change was measured, not assumed (docs/design-cutover.md section 3.1):
|
|
13
20
|
|
|
14
21
|
- `_Version` (rowversion) is on the OBJECT tables only - catalogs, documents, charts of
|
|
@@ -37,7 +44,7 @@ deletion left dangling, `seedstate` reseeds a balance whose journal moved, and t
|
|
|
37
44
|
`posted` markers are converged on the documents (that needs a token).
|
|
38
45
|
|
|
39
46
|
The watermark is kept in the tenant, in `df1c.watermark`, one row per loaded entity: the
|
|
40
|
-
`MIN_ACTIVE_ROWVERSION()` 1C reported before the rows
|
|
47
|
+
`MIN_ACTIVE_ROWVERSION()` 1C reported before the export read the rows (`store.horizon()`). Below it every
|
|
41
48
|
transaction had committed, so the next delta reads `_Version >= mark` and misses nothing.
|
|
42
49
|
In the tenant rather than a file, because it is a fact about the tenant's rows - written in
|
|
43
50
|
the transaction that wrote them, so neither can survive without the other, and a second
|
|
@@ -49,13 +56,17 @@ taken for a deletion. While 1C is the system of record, nothing is entered in th
|
|
|
49
56
|
from __future__ import print_function
|
|
50
57
|
|
|
51
58
|
import argparse
|
|
59
|
+
import collections
|
|
52
60
|
import datetime
|
|
53
61
|
import re
|
|
62
|
+
import struct
|
|
54
63
|
import sys
|
|
55
64
|
import time
|
|
56
65
|
|
|
57
66
|
import migrate
|
|
58
67
|
import paths
|
|
68
|
+
import store
|
|
69
|
+
import transactions
|
|
59
70
|
|
|
60
71
|
SCHEMA = "df1c"
|
|
61
72
|
KEYS_IN_LIST = 500
|
|
@@ -106,12 +117,6 @@ def unsettled(pg, module):
|
|
|
106
117
|
return set(r[0] for r in cur.fetchall())
|
|
107
118
|
|
|
108
119
|
|
|
109
|
-
def horizon(mssql):
|
|
110
|
-
"""Everything below this `_Version` is committed; anything written later is at or above."""
|
|
111
|
-
mssql.execute("SELECT MIN_ACTIVE_ROWVERSION()")
|
|
112
|
-
return bytes(mssql.fetchone()[0])
|
|
113
|
-
|
|
114
|
-
|
|
115
120
|
def record(pg, module, job, version, lower_only=False):
|
|
116
121
|
"""Set an entity's mark, inside the caller's transaction."""
|
|
117
122
|
ensure(pg)
|
|
@@ -182,9 +187,9 @@ class Infobase(object):
|
|
|
182
187
|
detection for objects, lines and movements.
|
|
183
188
|
"""
|
|
184
189
|
|
|
185
|
-
def __init__(self,
|
|
190
|
+
def __init__(self, export, source, since):
|
|
186
191
|
self.versions = {} # table -> [(raw key, raw version)] at or after `since`
|
|
187
|
-
self.now = horizon(
|
|
192
|
+
self.now = store.horizon()
|
|
188
193
|
started = time.time()
|
|
189
194
|
self.keys = 0
|
|
190
195
|
self.alive = []
|
|
@@ -195,15 +200,12 @@ class Infobase(object):
|
|
|
195
200
|
continue
|
|
196
201
|
if "_IDRRef" not in info["columns"]:
|
|
197
202
|
continue
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
% (name, hexed(since)))
|
|
203
|
-
self.versions[name] = [(bytes(k), bytes(v)) for k, v in mssql.fetchall()]
|
|
204
|
-
mssql.execute("SELECT _IDRRef FROM [%s]" % name)
|
|
203
|
+
export.execute('SELECT "_IDRRef", "_Version" FROM %s WHERE "_Version" >= %s'
|
|
204
|
+
% (store.quote(name), store.literal(since)))
|
|
205
|
+
self.versions[name] = [(bytes(k), bytes(v)) for k, v in export.fetchall()]
|
|
206
|
+
export.execute('SELECT "_IDRRef" FROM %s' % store.quote(name))
|
|
205
207
|
while True:
|
|
206
|
-
batch =
|
|
208
|
+
batch = export.fetchmany(50000)
|
|
207
209
|
if not batch:
|
|
208
210
|
break
|
|
209
211
|
self.keys += len(batch)
|
|
@@ -222,6 +224,19 @@ class Infobase(object):
|
|
|
222
224
|
keys = [k for k, v in rows if v >= mark]
|
|
223
225
|
if keys:
|
|
224
226
|
out[table_number(name)] = keys
|
|
227
|
+
# Recorders whose accounting entries were switched on or off: the document was not
|
|
228
|
+
# written, so its `_Version` says nothing, and `dump1c data --changes` names them.
|
|
229
|
+
# A run ending AT the mark counts too: entries have no rowversion, so switching them
|
|
230
|
+
# does not move the horizon, and a run can end on the very mark a load was taken at.
|
|
231
|
+
# Taking one too many only re-reads recorders that then compare unchanged.
|
|
232
|
+
for run in store.manifest().get("changes", []):
|
|
233
|
+
if bytes.fromhex(run["to"]) < mark:
|
|
234
|
+
continue
|
|
235
|
+
for table in run["tables"].values():
|
|
236
|
+
for number, keys in (table.get("switched") or {}).items():
|
|
237
|
+
held = out.setdefault(int(number), [])
|
|
238
|
+
seen = set(held)
|
|
239
|
+
held.extend(k for k in map(bytes.fromhex, keys) if k not in seen)
|
|
225
240
|
return out
|
|
226
241
|
|
|
227
242
|
def load_alive(self, pg):
|
|
@@ -244,7 +259,7 @@ def q(name):
|
|
|
244
259
|
return '"%s"' % name.replace('"', '""')
|
|
245
260
|
|
|
246
261
|
|
|
247
|
-
def stage(pg,
|
|
262
|
+
def stage(pg, export, job, columns, restricts):
|
|
248
263
|
"""Read the rows `restricts` select from 1C through the load's own conversions into a
|
|
249
264
|
temporary table shaped like the target. `restricts` is a list of alias -> condition
|
|
250
265
|
functions, each one query (a long key list is split), or [None] for the whole source."""
|
|
@@ -252,12 +267,12 @@ def stage(pg, mssql, job, columns, restricts):
|
|
|
252
267
|
cur.execute("DROP TABLE IF EXISTS pg_temp.df1c_stage")
|
|
253
268
|
cur.execute('CREATE TEMP TABLE df1c_stage AS SELECT %s FROM "%s"."%s" WITH NO DATA'
|
|
254
269
|
% (", ".join(q(c) for c in columns), job.schema, job.table))
|
|
255
|
-
select = ", ".join(
|
|
270
|
+
select = ", ".join(store.quote(p[1]) for p in job.pairs)
|
|
256
271
|
staged = 0
|
|
257
272
|
for restrict in restricts:
|
|
258
|
-
|
|
273
|
+
export.execute("SELECT %s FROM %s" % (select, migrate.reading(job, restrict)))
|
|
259
274
|
while True:
|
|
260
|
-
batch =
|
|
275
|
+
batch = export.fetchmany(migrate.CHUNK)
|
|
261
276
|
if not batch:
|
|
262
277
|
break
|
|
263
278
|
rows = migrate.converted(job, batch)
|
|
@@ -268,10 +283,10 @@ def stage(pg, mssql, job, columns, restricts):
|
|
|
268
283
|
|
|
269
284
|
|
|
270
285
|
def in_lists(keys, column, alias):
|
|
271
|
-
"""`alias.column IN (
|
|
286
|
+
"""`alias.column IN (from_hex(..), ...)`, one condition per slice of the key list."""
|
|
272
287
|
out = []
|
|
273
288
|
for at in range(0, len(keys), KEYS_IN_LIST):
|
|
274
|
-
listed = ", ".join(
|
|
289
|
+
listed = ", ".join(store.literal(k) for k in keys[at:at + KEYS_IN_LIST])
|
|
275
290
|
out.append(lambda a, listed=listed: "%s.%s IN (%s)" % (a, column, listed))
|
|
276
291
|
return out
|
|
277
292
|
|
|
@@ -285,7 +300,7 @@ def recorder_lists(changed, typed):
|
|
|
285
300
|
return in_lists(keys, "_RecorderRRef", None)
|
|
286
301
|
out = []
|
|
287
302
|
for number, keys in sorted(changed.items()):
|
|
288
|
-
tref = "
|
|
303
|
+
tref = store.literal(struct.pack(">I", number))
|
|
289
304
|
for cond in in_lists(keys, "_RecorderRRef", None):
|
|
290
305
|
out.append(lambda a, tref=tref, cond=cond:
|
|
291
306
|
"%s._RecorderTRef = %s AND %s" % (a, tref, cond(a)))
|
|
@@ -316,7 +331,7 @@ def has_missing(pg, job):
|
|
|
316
331
|
return any(c[0] == "is_missing" for c in migrate.table_columns(pg, job.schema, job.table))
|
|
317
332
|
|
|
318
333
|
|
|
319
|
-
def run_entity(pg,
|
|
334
|
+
def run_entity(pg, export, lookup, base, module, job, kind, detail, mark, apply, now):
|
|
320
335
|
res = Result(module, job, kind)
|
|
321
336
|
job = migrate.bind_enums(job, lookup)
|
|
322
337
|
columns = [p[0] for p in job.pairs] + ([job.derived] if job.derived else [])
|
|
@@ -329,8 +344,8 @@ def run_entity(pg, mssql, lookup, base, module, job, kind, detail, mark, apply,
|
|
|
329
344
|
|
|
330
345
|
if kind == "object":
|
|
331
346
|
key_column = detail
|
|
332
|
-
res.staged = stage(pg,
|
|
333
|
-
[lambda a: "%s._Version >= %s" % (a,
|
|
347
|
+
res.staged = stage(pg, export, job, columns,
|
|
348
|
+
[lambda a: "%s._Version >= %s" % (a, store.literal(mark))])
|
|
334
349
|
res.read = time.time() - started
|
|
335
350
|
started = time.time()
|
|
336
351
|
on = " AND ".join("t.%s = s.%s" % (q(k), q(k)) for k in keys)
|
|
@@ -407,7 +422,7 @@ def run_entity(pg, mssql, lookup, base, module, job, kind, detail, mark, apply,
|
|
|
407
422
|
cur.execute("CREATE INDEX ON pg_temp.df1c_owners (id)")
|
|
408
423
|
cur.execute("ANALYZE pg_temp.df1c_owners")
|
|
409
424
|
res.owners = len(changed_ids) + gone_owners
|
|
410
|
-
res.staged = stage(pg,
|
|
425
|
+
res.staged = stage(pg, export, job, columns, restricts) if restricts else 0
|
|
411
426
|
if not restricts:
|
|
412
427
|
cur.execute("DROP TABLE IF EXISTS pg_temp.df1c_stage")
|
|
413
428
|
cur.execute('CREATE TEMP TABLE df1c_stage AS SELECT %s FROM %s WITH NO DATA'
|
|
@@ -447,7 +462,7 @@ def run_entity(pg, mssql, lookup, base, module, job, kind, detail, mark, apply,
|
|
|
447
462
|
return res
|
|
448
463
|
|
|
449
464
|
# whole: read it all, compare by key.
|
|
450
|
-
res.staged = stage(pg,
|
|
465
|
+
res.staged = stage(pg, export, job, columns, [None])
|
|
451
466
|
res.read = time.time() - started
|
|
452
467
|
started = time.time()
|
|
453
468
|
on = " AND ".join("t.%s = s.%s" % (q(k), q(k)) for k in keys)
|
|
@@ -502,6 +517,14 @@ def begin(cur):
|
|
|
502
517
|
|
|
503
518
|
# --------------------------------------------------------------------------- after a module
|
|
504
519
|
|
|
520
|
+
SettleResult = collections.namedtuple("SettleResult", "status notes")
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def settlement_exit_code(outcomes, apply):
|
|
524
|
+
"""Dry runs only estimate; an applied but unfinished module must block cutover."""
|
|
525
|
+
return 2 if apply and any(o.status != "ready" for o in outcomes) else 0
|
|
526
|
+
|
|
527
|
+
|
|
505
528
|
def settle(pg, module, results, apply, mark_documents):
|
|
506
529
|
"""What a module needs once its rows moved: placeholders, balances, markers."""
|
|
507
530
|
import posted
|
|
@@ -513,11 +536,11 @@ def settle(pg, module, results, apply, mark_documents):
|
|
|
513
536
|
notes = []
|
|
514
537
|
settled = True
|
|
515
538
|
if apply and touched:
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
539
|
+
with transactions.writing(pg):
|
|
540
|
+
cur = pg.cursor()
|
|
541
|
+
cur.execute("SET LOCAL session_replication_role = 'replica'")
|
|
542
|
+
filled = migrate.fill_missing(pg, schemas, verbose=False)
|
|
543
|
+
pg.commit()
|
|
521
544
|
if filled:
|
|
522
545
|
notes.append("%d placeholder(s)" % sum(f[4] for f in filled))
|
|
523
546
|
|
|
@@ -526,9 +549,8 @@ def settle(pg, module, results, apply, mark_documents):
|
|
|
526
549
|
if reg.journal_entity not in touched:
|
|
527
550
|
continue
|
|
528
551
|
if apply:
|
|
529
|
-
pg
|
|
530
|
-
|
|
531
|
-
pg.autocommit = True
|
|
552
|
+
with transactions.writing(pg):
|
|
553
|
+
rows, written, _ = seedstate.seed(pg, reg)
|
|
532
554
|
off = seedstate.drift(pg.cursor(), reg)
|
|
533
555
|
settled = settled and not off
|
|
534
556
|
notes.append("reseeded %s.%s: %d tuple(s), check %s"
|
|
@@ -574,16 +596,17 @@ def settle(pg, module, results, apply, mark_documents):
|
|
|
574
596
|
else:
|
|
575
597
|
notes.append("would converge markers on %d document table(s)" % len(work))
|
|
576
598
|
if apply and touched and settled:
|
|
577
|
-
pg.cursor().execute("DELETE FROM %s.unsettled WHERE module = %%s
|
|
578
|
-
|
|
599
|
+
pg.cursor().execute("DELETE FROM %s.unsettled WHERE module = %%s AND entity = ANY(%%s)"
|
|
600
|
+
% SCHEMA, (module, sorted(touched)))
|
|
601
|
+
return SettleResult("dry_run" if not apply else ("ready" if settled else "partial"), notes)
|
|
579
602
|
|
|
580
603
|
|
|
581
604
|
# --------------------------------------------------------------------------- command
|
|
582
605
|
|
|
583
|
-
def since_date(
|
|
606
|
+
def since_date(export, source, day):
|
|
584
607
|
"""A watermark for estimation: the lowest `_Version` of any document dated on or after
|
|
585
608
|
`day`. Everything 1C wrote from about then on is at or above it."""
|
|
586
|
-
shifted = "%04d-%s" % (int(day[:4]) +
|
|
609
|
+
shifted = "%04d-%s" % (int(day[:4]) + store.year_offset(), day[5:])
|
|
587
610
|
lowest = None
|
|
588
611
|
for name, info in source.items():
|
|
589
612
|
if not name.startswith("_Document") or not OBJECT.match(name):
|
|
@@ -591,8 +614,9 @@ def since_date(mssql, source, day):
|
|
|
591
614
|
if "_Date_Time" not in info["columns"] or \
|
|
592
615
|
(info["columns"].get("_Version") or {}).get("type") != "timestamp":
|
|
593
616
|
continue
|
|
594
|
-
|
|
595
|
-
|
|
617
|
+
export.execute("SELECT MIN(_Version) FROM %s WHERE _Date_Time >= '%s'"
|
|
618
|
+
% (store.quote(name), shifted))
|
|
619
|
+
found = export.fetchone()[0]
|
|
596
620
|
if found is not None and (lowest is None or bytes(found) < lowest):
|
|
597
621
|
lowest = bytes(found)
|
|
598
622
|
if lowest is None:
|
|
@@ -641,8 +665,23 @@ def main():
|
|
|
641
665
|
print("%-12s %-40s %s %s" % (module, entity, hexed(version), at))
|
|
642
666
|
return 0
|
|
643
667
|
|
|
644
|
-
|
|
645
|
-
|
|
668
|
+
if store.horizon() is None:
|
|
669
|
+
raise SystemExit("delta: the export in %s has no horizon - %s.\n"
|
|
670
|
+
" Reload instead: `dump1c data`, then `migrate run <module> --truncate`."
|
|
671
|
+
% (paths.OUT, "a file infobase's row versions are per table"
|
|
672
|
+
if store.source_kind() == "file" else "a table in it was exported without one"))
|
|
673
|
+
# Every object table is read for its keys - a deletion is a key 1C no longer holds, and
|
|
674
|
+
# a recorder is any object - so one left out of the export is every row it owns taken
|
|
675
|
+
# for deleted. Asked here, by name, rather than as DuckDB's "Table _Acc33 does not exist".
|
|
676
|
+
exported = store.manifest()["tables"]
|
|
677
|
+
missing = sorted(n for n, info in source.items() if OBJECT.match(n) and n not in exported
|
|
678
|
+
and (info["columns"].get("_Version") or {}).get("type") == "timestamp")
|
|
679
|
+
if missing:
|
|
680
|
+
raise SystemExit("delta: %d object table(s) are not in the export (%s%s) - every object's keys"
|
|
681
|
+
" are needed.\n `1c.py dump1c data --objects` exports them all."
|
|
682
|
+
% (len(missing), ", ".join(missing[:5]), ", ..." if len(missing) > 5 else ""))
|
|
683
|
+
db = migrate.source_connect()
|
|
684
|
+
export, lookup = db.cursor(), db.cursor()
|
|
646
685
|
plan = []
|
|
647
686
|
for module in modules:
|
|
648
687
|
for job in migrate.jobs_for(pg, module, mapping, source, args.entity):
|
|
@@ -651,7 +690,7 @@ def main():
|
|
|
651
690
|
plan.append((module, job))
|
|
652
691
|
|
|
653
692
|
if args.baseline:
|
|
654
|
-
now = horizon(
|
|
693
|
+
now = store.horizon()
|
|
655
694
|
set_ = 0
|
|
656
695
|
cur = pg.cursor()
|
|
657
696
|
cur.execute("BEGIN")
|
|
@@ -669,7 +708,7 @@ def main():
|
|
|
669
708
|
override = None
|
|
670
709
|
if args.since:
|
|
671
710
|
override = (bytes.fromhex(args.since[2:]) if args.since.startswith("0x")
|
|
672
|
-
else since_date(
|
|
711
|
+
else since_date(export, source, args.since))
|
|
673
712
|
unmarked = [(m, j.entity) for m, j in plan if (m, j.entity) not in known]
|
|
674
713
|
if unmarked and override is None:
|
|
675
714
|
raise SystemExit("delta: %d entit(ies) have no watermark (e.g. %s.%s) - load them with"
|
|
@@ -681,9 +720,9 @@ def main():
|
|
|
681
720
|
|
|
682
721
|
lowest = min(mark_of(m, j) for m, j in plan)
|
|
683
722
|
t0 = time.time()
|
|
684
|
-
base = Infobase(
|
|
723
|
+
base = Infobase(export, source, lowest)
|
|
685
724
|
alive_seconds = base.load_alive(pg)
|
|
686
|
-
print("
|
|
725
|
+
print("export horizon %s, reading from %s%s" % (hexed(base.now), hexed(lowest),
|
|
687
726
|
" (--since %s)" % args.since if args.since else ""))
|
|
688
727
|
print("object keys: %d read in %.1fs, %.1fs into the tenant"
|
|
689
728
|
% (base.keys, base.seconds, alive_seconds))
|
|
@@ -704,13 +743,14 @@ def main():
|
|
|
704
743
|
print("-" * len(header))
|
|
705
744
|
totals = [0, 0, 0, 0, 0.0]
|
|
706
745
|
all_notes = []
|
|
746
|
+
outcomes = []
|
|
707
747
|
for module in modules:
|
|
708
748
|
results = []
|
|
709
749
|
for m, job in plan:
|
|
710
750
|
if m != module:
|
|
711
751
|
continue
|
|
712
752
|
kind, detail = classify(job, source, mapping[module][job.entity])
|
|
713
|
-
res = run_entity(pg,
|
|
753
|
+
res = run_entity(pg, export, lookup, base, module, job, kind, detail,
|
|
714
754
|
mark_of(module, job), args.apply, base.now)
|
|
715
755
|
results.append(res)
|
|
716
756
|
totals[0] += res.changed
|
|
@@ -724,7 +764,20 @@ def main():
|
|
|
724
764
|
res.owners if kind in ("lines", "movements") else "",
|
|
725
765
|
res.changed, res.new, res.deleted, res.read, res.compare, res.write))
|
|
726
766
|
sys.stdout.flush()
|
|
727
|
-
|
|
767
|
+
try:
|
|
768
|
+
outcome = settle(pg, module, results, args.apply, jwt if args.apply else None)
|
|
769
|
+
except transactions.ConnectionRecoveryError:
|
|
770
|
+
raise # Never continue on a connection whose cleanup failed.
|
|
771
|
+
except Exception as failure:
|
|
772
|
+
if not args.apply:
|
|
773
|
+
raise
|
|
774
|
+
transactions.recover(pg)
|
|
775
|
+
outcome = SettleResult("partial", ["settlement FAILED: %s: %s"
|
|
776
|
+
% (type(failure).__name__, failure)])
|
|
777
|
+
outcomes.append(outcome)
|
|
778
|
+
all_notes.append("%s: status %s%s" % (module, outcome.status,
|
|
779
|
+
"" if args.apply else " (estimate only, not a readiness verdict)"))
|
|
780
|
+
for note in outcome.notes:
|
|
728
781
|
all_notes.append("%s: %s" % (module, note))
|
|
729
782
|
print("-" * len(header))
|
|
730
783
|
print("%d entities changed %d new %d deleted %d (owners/recorders replaced %d)"
|
|
@@ -734,7 +787,11 @@ def main():
|
|
|
734
787
|
print(" " + note)
|
|
735
788
|
print("(rows of an entity with nothing changed and under 2s are not listed;"
|
|
736
789
|
" lines/movements count rows replaced as new/deleted)")
|
|
737
|
-
|
|
790
|
+
code = settlement_exit_code(outcomes, args.apply)
|
|
791
|
+
if code:
|
|
792
|
+
print("delta incomplete (%s): do not cut over; resolve the notes and rerun --apply."
|
|
793
|
+
% ", ".join(m for m, o in zip(modules, outcomes) if o.status != "ready"))
|
|
794
|
+
return code
|
|
738
795
|
|
|
739
796
|
|
|
740
797
|
if __name__ == "__main__":
|
|
@@ -26,6 +26,7 @@ import handlers
|
|
|
26
26
|
import meta
|
|
27
27
|
import migrate
|
|
28
28
|
import paths
|
|
29
|
+
import store
|
|
29
30
|
|
|
30
31
|
TARGET = os.path.join(paths.OUT, "docusage.json")
|
|
31
32
|
|
|
@@ -46,14 +47,14 @@ def documents():
|
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
def measure(cur, table):
|
|
49
|
-
cur.execute("SELECT COUNT(*), MAX(_Date_Time) FROM
|
|
50
|
+
cur.execute("SELECT COUNT(*), MAX(_Date_Time) FROM %s" % store.quote(table))
|
|
50
51
|
rows, last = cur.fetchone()
|
|
51
52
|
return rows, last
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
def main():
|
|
55
56
|
migrate.load_env()
|
|
56
|
-
cur = migrate.
|
|
57
|
+
cur = migrate.source_connect().cursor()
|
|
57
58
|
docs = documents()
|
|
58
59
|
newest = None
|
|
59
60
|
raw = []
|
|
@@ -68,8 +69,8 @@ def main():
|
|
|
68
69
|
recent = posted = 0
|
|
69
70
|
if rows:
|
|
70
71
|
cur.execute("SELECT SUM(CASE WHEN _Date_Time >= ? THEN 1 ELSE 0 END),"
|
|
71
|
-
" SUM(CASE WHEN _Posted =
|
|
72
|
-
% table, cutoff)
|
|
72
|
+
" SUM(CASE WHEN _Posted = %s THEN 1 ELSE 0 END) FROM %s"
|
|
73
|
+
% (store.literal(store.TRUE), store.quote(table)), cutoff)
|
|
73
74
|
recent, posted = [int(v or 0) for v in cur.fetchone()]
|
|
74
75
|
result.append({
|
|
75
76
|
"name": name,
|