@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
|
@@ -1,54 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
for name, state, access in
|
|
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(
|
|
140
|
-
|
|
141
|
-
print(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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(
|
|
158
|
-
|
|
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
|
|
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(
|
|
181
|
-
buf = read_file(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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("
|
|
243
|
-
|
|
244
|
-
ap.add_argument("--
|
|
245
|
-
|
|
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](
|
|
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.
|
|
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 =
|
|
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(
|
|
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(
|
|
292
|
+
tree = parse(load(root()) or "")
|
|
283
293
|
dbn = dbnames_index()
|
|
284
294
|
result = []
|
|
285
295
|
for group in collect_guid_groups(tree):
|