@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/dist/server.js +71 -3
  3. package/package.json +47 -48
  4. package/resources/docs/dsl-reference.md +1 -1
  5. package/resources/schemas/data-views.schema.json +50 -0
  6. package/skills/dforge-1c-import/SKILL.md +3 -2
  7. package/skills/dforge-1c-import/references/environment.md +2 -1
  8. package/skills/dforge-1c-import/references/loading.md +5 -2
  9. package/skills/dforge-1c-import/references/measurements.md +1 -1
  10. package/skills/dforge-1c-import/scripts/README.md +88 -7
  11. package/skills/dforge-1c-import/scripts/lib/__pycache__/attrs.cpython-313.pyc +0 -0
  12. package/skills/dforge-1c-import/scripts/lib/__pycache__/delta.cpython-313.pyc +0 -0
  13. package/skills/dforge-1c-import/scripts/lib/__pycache__/meta.cpython-313.pyc +0 -0
  14. package/skills/dforge-1c-import/scripts/lib/__pycache__/migrate.cpython-313.pyc +0 -0
  15. package/skills/dforge-1c-import/scripts/lib/__pycache__/paths.cpython-313.pyc +0 -0
  16. package/skills/dforge-1c-import/scripts/lib/__pycache__/posted.cpython-313.pyc +0 -0
  17. package/skills/dforge-1c-import/scripts/lib/__pycache__/posting.cpython-313.pyc +0 -0
  18. package/skills/dforge-1c-import/scripts/lib/__pycache__/seedstate.cpython-313.pyc +0 -0
  19. package/skills/dforge-1c-import/scripts/lib/__pycache__/store.cpython-313.pyc +0 -0
  20. package/skills/dforge-1c-import/scripts/lib/__pycache__/totals.cpython-313.pyc +0 -0
  21. package/skills/dforge-1c-import/scripts/lib/__pycache__/transactions.cpython-313.pyc +0 -0
  22. package/skills/dforge-1c-import/scripts/lib/attrs.py +2 -2
  23. package/skills/dforge-1c-import/scripts/lib/changes.py +326 -0
  24. package/skills/dforge-1c-import/scripts/lib/delta.py +109 -52
  25. package/skills/dforge-1c-import/scripts/lib/docusage.py +5 -4
  26. package/skills/dforge-1c-import/scripts/lib/dump1c.py +249 -92
  27. package/skills/dforge-1c-import/scripts/lib/external.py +4 -3
  28. package/skills/dforge-1c-import/scripts/lib/meta.py +12 -2
  29. package/skills/dforge-1c-import/scripts/lib/migrate.py +93 -74
  30. package/skills/dforge-1c-import/scripts/lib/onecd.py +269 -0
  31. package/skills/dforge-1c-import/scripts/lib/paths.py +21 -0
  32. package/skills/dforge-1c-import/scripts/lib/peek.py +3 -5
  33. package/skills/dforge-1c-import/scripts/lib/recorded.py +11 -11
  34. package/skills/dforge-1c-import/scripts/lib/recorders.py +9 -12
  35. package/skills/dforge-1c-import/scripts/lib/seedstate.py +21 -16
  36. package/skills/dforge-1c-import/scripts/lib/source_file.py +227 -0
  37. package/skills/dforge-1c-import/scripts/lib/source_mssql.py +158 -0
  38. package/skills/dforge-1c-import/scripts/lib/store.py +126 -0
  39. package/skills/dforge-1c-import/scripts/lib/totals.py +2 -4
  40. package/skills/dforge-1c-import/scripts/lib/transactions.py +28 -0
  41. package/skills/dforge-1c-import/scripts/project-template/.env.example +24 -0
  42. package/skills/dforge-1c-import/scripts/project-template/sources.py +10 -0
  43. package/skills/dforge-mcp-author/references/action-dsl.md +49 -46
  44. package/skills/dforge-mcp-author/references/data-views.md +38 -2
  45. package/skills/dforge-mcp-author/references/formulas.md +38 -6
  46. package/skills/dforge-mcp-author/references/jobs.md +1 -1
  47. package/skills/dforge-mcp-author/references/print-templates.md +30 -5
  48. package/skills/dforge-mcp-author/references/queries.md +17 -2
  49. package/skills/dforge-mcp-author/references/registers.md +3 -0
  50. package/skills/dforge-mcp-author/references/reports.md +1 -1
  51. package/skills/dforge-mcp-author/references/security.md +2 -0
  52. package/skills/dforge-mcp-author/references/validation-checklist.md +7 -8
@@ -1,54 +1,62 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
  """
4
- Read a 1C:Enterprise 8 infobase hosted on MS SQL Server *without* any 1C software.
4
+ Export a 1C:Enterprise 8 infobase to `out/` *without* any 1C software - the one layer that
5
+ reads 1C itself. Everything past it reads the export, so it does not matter which kind of
6
+ infobase the export came from.
7
+
8
+ Two kinds of infobase, one export:
9
+
10
+ a server infobase on MS SQL DF1C_SERVER + DF1C_DB (source_mssql.py)
11
+ a file infobase, 1Cv8.1CD DF1C_FILE=<path to .1CD> (source_file.py, onecd.py)
12
+
13
+ What it writes:
14
+
15
+ config/<file>.txt|.bin the configuration, one file per Config row, inflated (extract)
16
+ dbnames.json DBNames: GUID -> physical table/column name (dbnames)
17
+ schema.json every table, its columns in MS SQL's terms, row count (schema)
18
+ data/<table>.parquet the rows of every `_` table, as 1C stores them (data)
19
+ export.json the manifest: which infobase, when, its year offset,
20
+ and per table the rows and the horizon taken before them
5
21
 
6
22
  Everything needed to rebuild the data model lives in two service tables:
7
23
  Params -> row FileName='DBNames': GUID -> physical table/column name map
8
24
  Config -> the configuration itself, one row (8.2) or several PartNo rows (8.3) per file
25
+ File bodies are raw-deflate compressed (no zlib header), in both kinds of infobase.
9
26
 
10
- File bodies are raw-deflate compressed (no zlib header).
27
+ The rows are written as 1C stores them - dates still carrying a server infobase's year
28
+ offset, booleans as binary(1) - and the manifest records the offset, so a project query
29
+ written against the infobase reads the export unchanged.
11
30
 
12
- Credentials come from env DF1C_USER / DF1C_PASS; without them, Windows auth is used.
13
- Server and database default to DF1C_SERVER / DF1C_DB.
31
+ python 1c.py dump1c all # everything, in order
32
+ python 1c.py dump1c data _Reference12 ... # re-export these tables only
33
+ python 1c.py dump1c data --skip "ChngR|Settings"
34
+ python 1c.py dump1c data --changes # only what 1C changed since (MS SQL; changes.py)
35
+ python 1c.py dump1c probe # what the source is, before reading it
14
36
  """
15
37
  import argparse
16
38
  import codecs
39
+ import datetime
17
40
  import json
18
41
  import os
19
42
  import re
20
43
  import struct
21
44
  import sys
45
+ import time
22
46
  import zlib
23
47
 
24
48
  import paths
25
49
 
26
50
  OUT = paths.OUT
51
+ DATA = os.path.join(OUT, "data")
52
+ MANIFEST = os.path.join(OUT, "export.json")
53
+ CHUNK = 50000
27
54
 
28
- PREFERRED_DRIVERS = (
29
- "ODBC Driver 18 for SQL Server",
30
- "ODBC Driver 17 for SQL Server",
31
- "ODBC Driver 13 for SQL Server",
32
- "SQL Server Native Client 11.0",
33
- "SQL Server",
34
- )
35
-
36
-
37
- def connect(server, db):
38
- import pyodbc
39
- available = set(pyodbc.drivers())
40
- driver = next((d for d in PREFERRED_DRIVERS if d in available), None)
41
- if driver is None:
42
- raise RuntimeError("no SQL Server ODBC driver found: %r" % sorted(available))
43
- parts = ["DRIVER={%s}" % driver, "SERVER=%s" % server, "DATABASE=%s" % db,
44
- "TrustServerCertificate=yes"]
45
- user, pwd = os.environ.get("DF1C_USER"), os.environ.get("DF1C_PASS")
46
- if user:
47
- parts += ["UID=%s" % user, "PWD=%s" % pwd]
48
- else:
49
- parts += ["Trusted_Connection=yes"]
50
- sys.stderr.write("[i] %s / %s via %s\n" % (server, db, driver))
51
- return pyodbc.connect(";".join(parts), autocommit=True)
55
+ # What 1C keeps only as a consequence of other tables: register totals and the document
56
+ # journals. 53.7 M rows of a 313 M-row infobase, and no `_Version` to read their change by,
57
+ # so each `--changes` would read them all again - 14 minutes of a cutover window that has
58
+ # 10. Nothing loads them; `seedstate verify` reads totals, and says which to export.
59
+ DERIVED = re.compile(r"^_(AccumRgTn?|AccRgAT|AccRgCT|DocumentJournal)\d+$")
52
60
 
53
61
 
54
62
  def inflate(data):
@@ -64,23 +72,20 @@ def inflate(data):
64
72
  return data
65
73
 
66
74
 
67
- _HAS_PARTNO = {}
68
-
69
-
70
- def has_partno(cur, table):
71
- """8.3 splits large files across rows by PartNo; 8.2 stores one row per file."""
72
- if table not in _HAS_PARTNO:
73
- cur.execute("SELECT COUNT(*) FROM sys.columns WHERE object_id = OBJECT_ID(?) AND name = 'PartNo'", table)
74
- _HAS_PARTNO[table] = cur.fetchone()[0] > 0
75
- return _HAS_PARTNO[table]
76
-
77
-
78
- def read_file(cur, table, name):
79
- """Concatenate a file's parts in order, then inflate."""
80
- order = " ORDER BY PartNo" if has_partno(cur, table) else ""
81
- cur.execute("SELECT BinaryData FROM %s WHERE FileName = ?%s" % (table, order), name)
82
- blob = b"".join(bytes(r[0]) for r in cur.fetchall() if r[0] is not None)
83
- return inflate(blob)
75
+ def open_source():
76
+ """The infobase the environment names: a .1CD file, or a server infobase."""
77
+ path = os.environ.get("DF1C_FILE")
78
+ if path:
79
+ if os.environ.get("DF1C_SERVER") or os.environ.get("DF1C_DB"):
80
+ raise SystemExit("DF1C_FILE and DF1C_SERVER/DF1C_DB are both set - say which infobase to read")
81
+ if os.path.isdir(path):
82
+ path = os.path.join(path, "1Cv8.1CD")
83
+ import source_file
84
+ return source_file.FileSource(path)
85
+ import source_mssql
86
+ return source_mssql.MssqlSource(
87
+ paths.required("DF1C_SERVER", "which MS SQL server holds it (or DF1C_FILE for a file infobase)"),
88
+ paths.required("DF1C_DB", "which MS SQL infobase to read"))
84
89
 
85
90
 
86
91
  # --------------------------------------------------------------------------- DBNames
@@ -127,58 +132,75 @@ def physical_name(entry):
127
132
  return "_%s%d" % (kind, number)
128
133
 
129
134
 
135
+ # --------------------------------------------------------------------------- the manifest
136
+
137
+ def manifest():
138
+ if not os.path.exists(MANIFEST):
139
+ return None
140
+ with codecs.open(MANIFEST, "r", "utf-8") as f:
141
+ return json.load(f)
142
+
143
+
144
+ def begin(source):
145
+ """The manifest this run adds to. One export is one infobase: adding a second one's
146
+ tables to it would make a mix no check could tell from the real thing."""
147
+ described = source.describe()
148
+ held = manifest()
149
+ if held and held.get("source") != described:
150
+ raise SystemExit("out/ holds an export of %s, not %s.\n"
151
+ " Point DF1C_OUT somewhere else, or remove %s to start over."
152
+ % (json.dumps(held.get("source")), json.dumps(described), MANIFEST))
153
+ held = held or {"source": described, "tables": {}}
154
+ held["year_offset"] = source.year_offset
155
+ held["consistency"] = source.consistent
156
+ return held
157
+
158
+
159
+ def commit(held):
160
+ held["updated"] = datetime.datetime.now().replace(microsecond=0).isoformat()
161
+ write_json("export.json", held)
162
+
163
+
130
164
  # --------------------------------------------------------------------------- commands
131
165
 
132
- def cmd_databases(cur, args):
133
- """Which databases does this login actually see? Separates a bad password from missing rights."""
134
- cur.execute("SELECT name, state_desc, HAS_DBACCESS(name) FROM sys.databases ORDER BY name")
135
- for name, state, access in cur.fetchall():
166
+ def cmd_databases(source, args):
167
+ if not hasattr(source, "databases"):
168
+ raise SystemExit("databases: a file infobase is the one file")
169
+ for name, state, access in source.databases():
136
170
  print("%-44s %-10s access=%s" % (name, state, access))
137
171
 
138
172
 
139
- def cmd_probe(cur, args):
140
- cur.execute("SELECT @@VERSION")
141
- print(cur.fetchone()[0].splitlines()[0])
142
- for table in ("Config", "ConfigSave", "Params", "DBSchema", "_YearOffset"):
143
- try:
144
- cur.execute("SELECT COUNT(*) FROM [%s]" % table)
145
- print(" %-12s rows=%s" % (table, cur.fetchone()[0]))
146
- except Exception as exc:
147
- print(" %-12s -- %s" % (table, str(exc).split("]")[-1].strip()[:70]))
148
- cur.execute("""SELECT LEFT(name, PATINDEX('%[0-9]%', name + '0') - 1) AS kind, COUNT(*)
149
- FROM sys.tables WHERE name LIKE '[_]%'
150
- GROUP BY LEFT(name, PATINDEX('%[0-9]%', name + '0') - 1)
151
- ORDER BY 2 DESC""")
173
+ def cmd_probe(source, args):
174
+ print(json.dumps(source.describe(), ensure_ascii=False))
175
+ print("year offset %d" % source.year_offset)
176
+ if hasattr(source, "probe"):
177
+ for line in source.probe():
178
+ print(line)
179
+ families = {}
180
+ for name, _, rows in source.tables():
181
+ if name.startswith("_"):
182
+ kind = re.match(r"^_([A-Za-z]+)", name).group(1)
183
+ families[kind] = families.get(kind, 0) + 1
152
184
  print("\n physical table families:")
153
- for kind, n in cur.fetchall():
185
+ for kind, n in sorted(families.items(), key=lambda kv: -kv[1]):
154
186
  print(" %-24s %d" % (kind, n))
155
187
 
156
188
 
157
- def cmd_schema(cur, args):
158
- cur.execute("""SELECT t.name, c.name, ty.name, c.max_length, c.precision, c.scale, c.is_nullable
159
- FROM sys.tables t
160
- JOIN sys.columns c ON c.object_id = t.object_id
161
- JOIN sys.types ty ON ty.user_type_id = c.user_type_id
162
- ORDER BY t.name, c.column_id""")
163
- tables = {}
164
- for tn, cn, ty, ml, pr, sc, nullable in cur.fetchall():
165
- tables.setdefault(tn, []).append({
166
- "column": cn, "type": ty, "len": ml,
167
- "precision": pr, "scale": sc, "nullable": bool(nullable),
168
- })
169
- cur.execute("""SELECT t.name, SUM(p.rows) FROM sys.tables t
170
- JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)
171
- GROUP BY t.name""")
172
- rows = dict(cur.fetchall())
189
+ def cmd_schema(source, args):
190
+ tables = source.tables()
173
191
  write_json("schema.json", {"tables": [
174
- {"name": name, "rows": rows.get(name), "columns": cols}
175
- for name, cols in sorted(tables.items())
192
+ {"name": name, "rows": rows, "columns": cols} for name, cols, rows in sorted(tables)
176
193
  ]})
194
+ guessed = getattr(getattr(source, "names", None), "guessed", None)
195
+ if guessed:
196
+ held = begin(source)
197
+ held["names_guessed"] = sorted(guessed)
198
+ commit(held)
177
199
  print("tables=%d" % len(tables))
178
200
 
179
201
 
180
- def cmd_dbnames(cur, args):
181
- buf = read_file(cur, "Params", "DBNames")
202
+ def cmd_dbnames(source, args):
203
+ buf = source.read_file("Params", "DBNames")
182
204
  save("dbnames.bin", buf)
183
205
  entries = parse_dbnames(buf)
184
206
  for entry in entries:
@@ -192,15 +214,14 @@ def cmd_dbnames(cur, args):
192
214
  print(" %-28s %d" % (kind, n))
193
215
 
194
216
 
195
- def cmd_extract(cur, args):
217
+ def cmd_extract(source, args):
196
218
  target = os.path.join(OUT, "config")
197
219
  if not os.path.isdir(target):
198
220
  os.makedirs(target)
199
- cur.execute("SELECT DISTINCT FileName FROM Config")
200
- names = [r[0] for r in cur.fetchall()]
221
+ names = source.files("Config")
201
222
  text = binary = 0
202
223
  for i, name in enumerate(names):
203
- buf = read_file(cur, "Config", name)
224
+ buf = source.read_file("Config", name)
204
225
  safe = re.sub(r"[^0-9A-Za-z._-]", "_", name)[:120]
205
226
  if buf.startswith(codecs.BOM_UTF8) or b"{" in buf[:64]:
206
227
  suffix, text = ".txt", text + 1
@@ -213,6 +234,135 @@ def cmd_extract(cur, args):
213
234
  print("files=%d text=%d binary=%d -> %s" % (len(names), text, binary, target))
214
235
 
215
236
 
237
+ def export_table(source, name, columns, expected=None):
238
+ """One table's rows, in full, into `data/<name>.parquet`. The number written."""
239
+ import pyarrow.parquet as pq
240
+ if not os.path.isdir(DATA):
241
+ os.makedirs(DATA)
242
+ arrow = arrow_schema(columns)
243
+ tmp = os.path.join(DATA, name + ".parquet.tmp")
244
+ writer = pq.ParquetWriter(tmp, arrow, compression="zstd")
245
+ written = 0
246
+ try:
247
+ for batch in source.rows(name, [c["column"] for c in columns], CHUNK):
248
+ writer.write_table(arrow_batch(arrow, columns, batch))
249
+ written += len(batch)
250
+ if (expected or 0) > CHUNK:
251
+ sys.stderr.write("\r %-30s %9d / %d" % (name, written, expected))
252
+ finally:
253
+ writer.close()
254
+ if (expected or 0) > CHUNK:
255
+ sys.stderr.write("\n")
256
+ os.replace(tmp, os.path.join(DATA, name + ".parquet"))
257
+ return written
258
+
259
+
260
+ def cmd_data(source, args):
261
+ held = begin(source)
262
+ schema = dict((name, (cols, rows)) for name, cols, rows in source.tables())
263
+ if args.changes:
264
+ import changes
265
+ try:
266
+ changes.run(source, held, schema, args,
267
+ lambda name, columns: export_table(source, name, columns, schema[name][1]))
268
+ finally:
269
+ commit(held)
270
+ return
271
+ if args.since:
272
+ raise SystemExit("data: --since goes with --changes")
273
+ named = list(args.tables)
274
+ if args.objects:
275
+ # Every catalog, document and chart - what `delta` reads the keys of. Small: 407
276
+ # tables and 1.6 M rows of a 313 M-row infobase.
277
+ import changes
278
+ objects = [n for n in sorted(schema) if n not in named
279
+ and changes.classify(n, schema[n][0], schema)[0] == "object"]
280
+ if not objects:
281
+ # A file infobase: no rowversion, so nothing is an object in that sense - and
282
+ # an empty list would otherwise mean "every table".
283
+ raise SystemExit("data --objects: no table here has a rowversion - this is for an"
284
+ " MS SQL export, which `delta` needs")
285
+ named += objects
286
+ args.tables = named
287
+ wanted = args.tables or sorted(n for n in schema if n.startswith("_")
288
+ and (args.derived or not DERIVED.match(n)))
289
+ if not args.tables and not args.derived:
290
+ left = [n for n in schema if DERIVED.match(n)]
291
+ print("left out %d table(s) 1C derives from others - totals, journals (%d rows);"
292
+ " name one, or --derived, to export it" % (len(left), sum(schema[n][1] or 0 for n in left)))
293
+ unknown = [t for t in wanted if t not in schema]
294
+ if unknown:
295
+ raise SystemExit("data: no such table: %s" % ", ".join(unknown))
296
+ if args.tables:
297
+ # A partial export still takes every enum: they are a few rows each, and the loader
298
+ # decodes an enum column by reading its table - left out, a module's load stops at
299
+ # its first enum with "_Enum727 does not exist".
300
+ enums = [n for n in sorted(schema) if re.match(r"^_Enum\d+$", n) and n not in wanted]
301
+ wanted += enums
302
+ if args.skip:
303
+ wanted = [t for t in wanted if not re.search(args.skip, t)]
304
+ started, total = time.time(), 0
305
+ for at, name in enumerate(wanted):
306
+ columns, expected = schema[name]
307
+ horizon = source.horizon()
308
+ written = export_table(source, name, columns, expected)
309
+ held["tables"][name] = {"rows": written,
310
+ "horizon": horizon.hex() if horizon is not None else None,
311
+ "exported": datetime.datetime.now().replace(microsecond=0).isoformat()}
312
+ total += written
313
+ # The manifest is written as the tables are, so an export stopped half-way says
314
+ # truthfully what it holds.
315
+ if at % 50 == 0 or at == len(wanted) - 1:
316
+ commit(held)
317
+ print("tables=%d rows=%d in %.0fs -> %s" % (len(wanted), total, time.time() - started, DATA))
318
+
319
+
320
+ def cmd_all(source, args):
321
+ for step in (cmd_dbnames, cmd_schema, cmd_extract, cmd_data):
322
+ step(source, args)
323
+
324
+
325
+ # --------------------------------------------------------------------------- parquet
326
+
327
+ def arrow_type(column):
328
+ """The schema dump's MS SQL type -> how the export stores it."""
329
+ import pyarrow as pa
330
+ kind = column["type"]
331
+ if kind in ("binary", "varbinary", "image", "timestamp", "rowversion"):
332
+ return pa.binary()
333
+ if kind in ("nchar", "nvarchar", "ntext", "char", "varchar", "text", "sysname", "uniqueidentifier"):
334
+ return pa.string()
335
+ if kind in ("numeric", "decimal"):
336
+ return pa.decimal128(column.get("precision") or 38, column.get("scale") or 0)
337
+ if kind in ("datetime", "datetime2", "smalldatetime", "date"):
338
+ return pa.timestamp("us")
339
+ if kind in ("int", "smallint", "tinyint", "bigint"):
340
+ return pa.int64()
341
+ if kind == "bit":
342
+ return pa.bool_()
343
+ if kind in ("float", "real"):
344
+ return pa.float64()
345
+ return pa.string()
346
+
347
+
348
+ def arrow_schema(columns):
349
+ import pyarrow as pa
350
+ return pa.schema([pa.field(c["column"], arrow_type(c)) for c in columns])
351
+
352
+
353
+ def arrow_batch(arrow, columns, batch):
354
+ import pyarrow as pa
355
+ arrays = []
356
+ for i, field in enumerate(arrow):
357
+ values = [row[i] for row in batch]
358
+ if pa.types.is_string(field.type):
359
+ values = [v if v is None or isinstance(v, str) else str(v) for v in values]
360
+ elif pa.types.is_binary(field.type):
361
+ values = [v if v is None else bytes(v) for v in values]
362
+ arrays.append(pa.array(values, type=field.type))
363
+ return pa.Table.from_arrays(arrays, schema=arrow)
364
+
365
+
216
366
  def save(name, data):
217
367
  if not os.path.isdir(OUT):
218
368
  os.makedirs(OUT)
@@ -233,18 +383,25 @@ COMMANDS = {
233
383
  "schema": cmd_schema,
234
384
  "dbnames": cmd_dbnames,
235
385
  "extract": cmd_extract,
386
+ "data": cmd_data,
387
+ "all": cmd_all,
236
388
  }
237
389
 
238
390
 
239
391
  def main():
240
392
  ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
241
393
  ap.add_argument("command", choices=sorted(COMMANDS))
242
- ap.add_argument("--server", default=os.environ.get("DF1C_SERVER")
243
- or paths.required("DF1C_SERVER", "which MS SQL server holds it"))
244
- ap.add_argument("--db", default=os.environ.get("DF1C_DB")
245
- or paths.required("DF1C_DB", "which MS SQL infobase to read"))
394
+ ap.add_argument("tables", nargs="*", help="data: only these tables")
395
+ ap.add_argument("--skip", help="data: leave out tables matching this regex")
396
+ ap.add_argument("--objects", action="store_true",
397
+ help="data: every object table (catalogs, documents, charts) besides the named ones")
398
+ ap.add_argument("--derived", action="store_true",
399
+ help="data: export totals and journals too (left out by default)")
400
+ ap.add_argument("--changes", action="store_true",
401
+ help="data: read only what 1C changed since the export (MS SQL; see changes.py)")
402
+ ap.add_argument("--since", help="data --changes: from this day (YYYY-MM-DD) or rowversion (0x...)")
246
403
  args = ap.parse_args()
247
- COMMANDS[args.command](connect(args.server, args.db).cursor(), args)
404
+ COMMANDS[args.command](open_source(), args)
248
405
 
249
406
 
250
407
  if __name__ == "__main__":
@@ -40,6 +40,7 @@ import attrs
40
40
  import meta
41
41
  import migrate
42
42
  import paths
43
+ import store
43
44
  import unpack
44
45
 
45
46
  CONTAINER = b"\xff\xff\xff\x7f"
@@ -171,10 +172,10 @@ def main():
171
172
  enum_table = index[enum_guid]["physical"]
172
173
  kinds = attrs.enum_values(meta.parse(meta.load(enum_guid)), u"ВидыДополнительныхВнешнихОбработок")
173
174
 
174
- cur = migrate.mssql_connect().cursor()
175
+ cur = migrate.source_connect().cursor()
175
176
  cur.execute("SELECT t._Code, t._Description, t._Marked, e._EnumOrder, t.%s FROM %s t "
176
- "LEFT JOIN %s e ON e._IDRRef = t.%sRRef WHERE t._Folder = 0x01"
177
- % (storage, table, enum_table, kind))
177
+ "LEFT JOIN %s e ON e._IDRRef = t.%sRRef WHERE t._Folder = %s"
178
+ % (storage, table, enum_table, kind, store.literal(store.TRUE)))
178
179
  root = os.path.join(paths.OUT, "external")
179
180
  index_rows, skipped = [], []
180
181
  for code, name, marked, order, blob in cur.fetchall():
@@ -95,6 +95,16 @@ def parse(text):
95
95
  return read_value() if pos < n else []
96
96
 
97
97
 
98
+ def root():
99
+ """The configuration's own object: `root` names it, `{2,<guid>,}`. The GUID is the
100
+ configuration's, so it differs from one infobase to the next."""
101
+ match = re.search(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}", load("root") or "")
102
+ if not match:
103
+ raise SystemExit("meta: %s has no root - run `1c.py dump1c extract` first"
104
+ % os.path.join(CONFIG, "root.txt"))
105
+ return match.group(0)
106
+
107
+
98
108
  def load(guid):
99
109
  path = os.path.join(CONFIG, guid + ".txt")
100
110
  if not os.path.isfile(path):
@@ -150,7 +160,7 @@ def dbnames_index():
150
160
 
151
161
  def cmd_kinds(args):
152
162
  """Each group's family is whatever DBNames calls its members - no guessing from counts."""
153
- tree = parse(load("1e4190e9-76c2-456e-a607-4d817110ffd9") or "")
163
+ tree = parse(load(root()) or "")
154
164
  groups = collect_guid_groups(tree)
155
165
  dbn = dbnames_index()
156
166
 
@@ -279,7 +289,7 @@ def cmd_allobjects(args):
279
289
  common modules, roles and subsystems are missing from it - and they hold most of the
280
290
  configuration's code.
281
291
  """
282
- tree = parse(load("1e4190e9-76c2-456e-a607-4d817110ffd9") or "")
292
+ tree = parse(load(root()) or "")
283
293
  dbn = dbnames_index()
284
294
  result = []
285
295
  for group in collect_guid_groups(tree):