@dforge-core/dforge-mcp 0.2.45 → 0.2.47

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 (83) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +2 -1
  3. package/dist/server.js +71 -3
  4. package/package.json +3 -3
  5. package/resources/docs/dsl-reference.md +1 -1
  6. package/resources/schemas/data-views.schema.json +50 -0
  7. package/scripts/install-skills.mjs +1 -0
  8. package/skills/dforge-1c-import/SKILL.md +375 -0
  9. package/skills/dforge-1c-import/references/environment.md +98 -0
  10. package/skills/dforge-1c-import/references/loading.md +164 -0
  11. package/skills/dforge-1c-import/references/measurements.md +210 -0
  12. package/skills/dforge-1c-import/references/print-forms.md +148 -0
  13. package/skills/dforge-1c-import/scripts/1c.py +178 -0
  14. package/skills/dforge-1c-import/scripts/README.md +164 -0
  15. package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
  16. package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
  17. package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
  18. package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
  19. package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
  20. package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
  21. package/skills/dforge-1c-import/scripts/lib/changes.py +326 -0
  22. package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
  23. package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
  24. package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
  25. package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
  26. package/skills/dforge-1c-import/scripts/lib/delta.py +770 -0
  27. package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
  28. package/skills/dforge-1c-import/scripts/lib/docusage.py +106 -0
  29. package/skills/dforge-1c-import/scripts/lib/dump1c.py +408 -0
  30. package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
  31. package/skills/dforge-1c-import/scripts/lib/external.py +202 -0
  32. package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
  33. package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
  34. package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
  35. package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
  36. package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
  37. package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
  38. package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
  39. package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
  40. package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
  41. package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
  42. package/skills/dforge-1c-import/scripts/lib/meta.py +325 -0
  43. package/skills/dforge-1c-import/scripts/lib/migrate.py +1318 -0
  44. package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
  45. package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
  46. package/skills/dforge-1c-import/scripts/lib/onecd.py +269 -0
  47. package/skills/dforge-1c-import/scripts/lib/paths.py +111 -0
  48. package/skills/dforge-1c-import/scripts/lib/peek.py +68 -0
  49. package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
  50. package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
  51. package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
  52. package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
  53. package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
  54. package/skills/dforge-1c-import/scripts/lib/recorders.py +91 -0
  55. package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
  56. package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
  57. package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
  58. package/skills/dforge-1c-import/scripts/lib/seedstate.py +493 -0
  59. package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
  60. package/skills/dforge-1c-import/scripts/lib/source_file.py +227 -0
  61. package/skills/dforge-1c-import/scripts/lib/source_mssql.py +158 -0
  62. package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
  63. package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
  64. package/skills/dforge-1c-import/scripts/lib/store.py +126 -0
  65. package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
  66. package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
  67. package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
  68. package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
  69. package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
  70. package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
  71. package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
  72. package/skills/dforge-1c-import/scripts/project-template/.env.example +24 -0
  73. package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
  74. package/skills/dforge-1c-import/scripts/project-template/sources.py +24 -0
  75. package/skills/dforge-mcp-author/SKILL.md +5 -0
  76. package/skills/dforge-mcp-author/references/action-dsl.md +1 -1
  77. package/skills/dforge-mcp-author/references/data-views.md +38 -2
  78. package/skills/dforge-mcp-author/references/formulas.md +32 -1
  79. package/skills/dforge-mcp-author/references/print-templates.md +30 -5
  80. package/skills/dforge-mcp-author/references/queries.md +17 -2
  81. package/skills/dforge-mcp-author/references/registers.md +1 -0
  82. package/skills/dforge-mcp-author/references/reports.md +1 -1
  83. package/skills/dforge-mcp-author/references/validation-checklist.md +1 -0
@@ -0,0 +1,227 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ A file infobase (`1Cv8.1CD`) as an export source: `onecd` reads it, this makes what it
4
+ reads look like what a server infobase gives, so nothing downstream asks which one it was.
5
+
6
+ Three differences are folded here and nowhere else:
7
+
8
+ - NAMES. A file infobase keeps every name in capitals - `_REFERENCE12`, `_FLD345RREF` - where
9
+ MS SQL has `_Reference12`, `_Fld345RRef`, and the column map, the project's sources and
10
+ the library all spell them the MS SQL way. The spelling is put back from DBNames, which
11
+ carries every numbered kind in its own case (`AccumRgT`, `ReferenceChngR`, `VT`, `Fld`),
12
+ plus the fixed names 1C gives the standard columns. A name neither covers is a service
13
+ table's own business that nothing reads; it is capitalised and counted in the manifest.
14
+ - TYPES. The schema is written in MS SQL's vocabulary - `binary(16)`, `nvarchar`, `numeric`,
15
+ `datetime`, `len` in bytes as `sys.columns` gives it - because that is what the generator
16
+ and the checks read.
17
+ - VALUES. A boolean is `binary(1)` 0x01 / 0x00 as on MS SQL; the empty date is 0001-01-01.
18
+ A file infobase has no year offset: its dates are the real ones, and the manifest says so.
19
+
20
+ What does NOT carry over is the row version. In a file `_Version` is a per-table counter,
21
+ not MS SQL's database-wide rowversion, so a file export has no horizon and `delta` cannot
22
+ read changes off it. It is exported as `binary(16)`, for completeness only.
23
+ """
24
+ import os
25
+ import re
26
+ import struct
27
+
28
+ import dump1c
29
+ import onecd
30
+
31
+ SERVICE = dict((n.upper(), n) for n in (
32
+ "Config", "ConfigSave", "Params", "Files", "DBSchema", "IBVersion", "v8users", "_YearOffset",
33
+ "FileName", "Creation", "Modified", "Attributes", "DataSize", "BinaryData", "PartNo",
34
+ "SerializedData",
35
+ # v8users and IBVersion
36
+ "ID", "Name", "Descr", "OSName", "Changed", "RolesID", "Show", "Data", "EAuth", "AdmRole",
37
+ "UsSprH", "PlatformVersionReq"))
38
+
39
+ # The un-numbered segments of 1C's names as MS SQL spells them, between underscores:
40
+ # `_Fld12_TYPE` is `Fld12` and `TYPE`. Harvested from a server infobase's whole schema
41
+ # (2,668 tables, 28,715 columns) rather than remembered, so a list that misses one is
42
+ # fixed by harvesting again. Where two spellings fold to one - `UserId` and `UserID` both
43
+ # occur - the commoner one is here and `migrate` matches the other regardless of case.
44
+ WORDS = dict((w.upper(), w) for w in (
45
+ "APDateFrom", "APDateTill", "AccountCtRRef", "AccountDtRRef", "AccountRRef", "ActionPeriod",
46
+ "ActivationCondition", "Active", "ActualPeriod", "BPDateFrom", "BPDateTill", "BaseAsAct",
47
+ "BaseCK", "BaseCKBaseCK", "BaseCKBaseCKRRef", "BaseCKLineNo", "BusinessProcess", "CKRefRRef",
48
+ "CalcKindRRef", "Code", "Completed", "ConfigChngR", "ConnectPrms", "ConstID", "Correspond",
49
+ "Date", "Description", "DimHash", "DimIsMetadata", "DimKindRRef", "DisplNumber", "DisplacedCK",
50
+ "DisplacedCKDisplCKRRef", "DisplacedCKLineNo", "DocumentRRef", "DocumentTRef", "EDHashCt",
51
+ "EDHashDt", "EnumOrder", "Executed", "ExtDataSrcId", "ExtProps", "FileName", "FinishTime",
52
+ "Folder", "HeadTaskRRef", "ID", "IDRRef", "IsMetadata", "JobKey", "KeyField", "Kind",
53
+ "KindRRef", "LeadingCK", "LeadingCKLeadingCK", "LeadingCKLineNo", "LineNo", "MDObjID",
54
+ "Marked", "MessageNo", "MetadataID", "MinPeriod", "Name", "NodeRRef", "NodeTRef", "Number",
55
+ "NumberPrefix", "ObjectKey", "OffBalance", "OrderField", "OwnerID", "OwnerIDRRef",
56
+ "Parameters", "ParentIDRRef", "Period", "Periodicity", "Point", "Posted", "Predefined",
57
+ "PredefinedBaseCKTableLine", "PredefinedDisplCKTableLine", "PredefinedID",
58
+ "PredefinedLeadingCKTableLine", "PredefinedObj", "ReceivedNo", "RecordKey", "RecordKind",
59
+ "Recorder", "RecorderRRef", "RecorderTRef", "RegID", "RepetitionFactor",
60
+ "RestartAttemptNumber", "RestartCount", "RestartPeriod", "RoutePointOrder", "SentNo",
61
+ "SettingsData", "SettingsKey", "SettingsPresentation", "SimpleKey", "SliceUsing", "Splitter",
62
+ "StartTime", "Started", "State", "Storno", "TurnoverOnly", "URL", "URLHash", "Use",
63
+ "UseSplitter", "UseTotals", "UserId", "UserName", "Value", "Value1", "Value2", "Value3",
64
+ "Value4", "Version", "YearOffset",
65
+ # a composite reference's parts, and a column's own type tag
66
+ "RRRef", "RTRef", "RRef", "TRef", "L", "N", "T", "S", "B"))
67
+ # What follows a numbered kind inside one segment: `_Fld12RRef`, `_Fld12DtRRef`.
68
+ SUFFIXES = ("CtRRef", "DtRRef", "RRef", "TRef", "Ct", "Dt", "")
69
+
70
+
71
+ class Names(object):
72
+ """Upper-case 1CD names -> the MS SQL spelling, from DBNames' kinds."""
73
+
74
+ def __init__(self, kinds):
75
+ self.kinds = sorted(set(kinds), key=len, reverse=True)
76
+ self.numbered = [(re.compile("^%s(\\d+)(%s)$" % (k.upper(), "|".join(s.upper() for s in SUFFIXES))), k)
77
+ for k in self.kinds]
78
+ self.guessed = set()
79
+
80
+ def __call__(self, name):
81
+ if name in SERVICE:
82
+ return SERVICE[name]
83
+ parts = name.replace("DATE_TIME", "DATE\x00TIME").split("_")
84
+ out = []
85
+ for at, part in enumerate(parts):
86
+ if part == "DATE\x00TIME":
87
+ out.append("Date_Time")
88
+ elif not part:
89
+ out.append(part)
90
+ elif part == "TYPE":
91
+ # A column of its own is `_Type` (a characteristic's value type); after
92
+ # another segment it is a composite reference's tag, `_Fld12_TYPE`.
93
+ out.append("Type" if at == 1 else "TYPE")
94
+ else:
95
+ out.append(self.segment(part, name))
96
+ return "_".join(out)
97
+
98
+ def segment(self, part, name):
99
+ for pattern, kind in self.numbered:
100
+ match = pattern.match(part)
101
+ if match:
102
+ suffix = next(s for s in SUFFIXES if s.upper() == match.group(2))
103
+ return kind + match.group(1) + suffix
104
+ if part in WORDS:
105
+ return WORDS[part]
106
+ # A kind with no number of its own: a service table such as `_AccOpt`.
107
+ for kind in self.kinds:
108
+ if part == kind.upper():
109
+ return kind
110
+ self.guessed.add(name)
111
+ match = re.match(r"^([A-Z]+)(\d*)(.*)$", part)
112
+ return (match.group(1).capitalize() + match.group(2) + match.group(3)) if match else part
113
+
114
+
115
+ def numeric_bytes(precision):
116
+ """What `sys.columns.max_length` says for numeric(p): the schema dump carries it as-is."""
117
+ return 5 if precision <= 9 else 9 if precision <= 19 else 13 if precision <= 28 else 17
118
+
119
+
120
+ def column_type(field):
121
+ """A 1CD field -> the schema dump's (type, len, precision, scale) in MS SQL terms."""
122
+ kind, n = field.type, field.length
123
+ if kind == "B":
124
+ return "binary", n, 0, 0
125
+ if kind == "L":
126
+ return "binary", 1, 0, 0
127
+ if kind == "N":
128
+ return "numeric", numeric_bytes(n), n, field.precision
129
+ if kind == "NC":
130
+ return "nchar", n * 2, 0, 0
131
+ if kind == "NVC":
132
+ return "nvarchar", n * 2, 0, 0
133
+ if kind == "NT":
134
+ return "nvarchar", -1, 0, 0
135
+ if kind == "I":
136
+ return "varbinary", -1, 0, 0
137
+ if kind == "DT":
138
+ return "datetime", 8, 23, 3
139
+ if kind == "RV":
140
+ return "binary", 16, 0, 0
141
+ raise ValueError("1CD field type %s" % kind)
142
+
143
+
144
+ def canonical(field):
145
+ """A function turning one of `onecd`'s values for this field into MS SQL's."""
146
+ if field.type == "L":
147
+ return lambda v: None if v is None else (b"\x01" if v else b"\x00")
148
+ if field.type == "RV":
149
+ return lambda v: None if v is None else struct.pack(">4I", *v)
150
+ return None
151
+
152
+
153
+ class FileSource(object):
154
+ kind = "file"
155
+ year_offset = 0
156
+ consistent = "the file as it was read; copy it, or close the infobase, before exporting"
157
+
158
+ def __init__(self, path):
159
+ self.path = os.path.abspath(path)
160
+ self.db = onecd.Database(self.path)
161
+ self._files = {}
162
+ self._tables = None
163
+ self.names = Names(e["type"] for e in dump1c.parse_dbnames(self.read_file("Params", "DBNames")))
164
+ self.by_name = dict((self.names(t), t) for t in self.db.tables)
165
+
166
+ def describe(self):
167
+ return {"kind": self.kind, "path": self.path, "format": self.db.version,
168
+ "page_size": self.db.page_size, "locale": self.db.locale}
169
+
170
+ def _table(self, name):
171
+ return self.db.tables[self.by_name[name] if name in self.by_name else name.upper()]
172
+
173
+ def _service(self, table):
174
+ """A service table's files, {name: [(part, compressed body)]}, read in one pass: a
175
+ blob is reached only through its row, so a read per file would read them all again."""
176
+ table = table.upper()
177
+ if table not in self._files:
178
+ found = {}
179
+ for name, part, body in self.db.tables[table].rows(["FILENAME", "PARTNO", "BINARYDATA"]):
180
+ found.setdefault(name, []).append((part or 0, body or b""))
181
+ self._files[table] = found
182
+ return self._files[table]
183
+
184
+ def files(self, table):
185
+ return sorted(self._service(table))
186
+
187
+ def read_file(self, table, name):
188
+ parts = sorted(self._service(table).get(name, ()))
189
+ return dump1c.inflate(b"".join(body for _, body in parts))
190
+
191
+ def tables(self):
192
+ """[(name, columns, rows)] - the schema dump's shape, every table in the file.
193
+
194
+ Counting a table's rows reads all of it - a file keeps no row count - so this is
195
+ worked out once per run, however many commands of `dump1c all` ask for it."""
196
+ if self._tables is None:
197
+ self._tables = self._read_tables()
198
+ return self._tables
199
+
200
+ def _read_tables(self):
201
+ out = []
202
+ for raw, table in self.db.tables.items():
203
+ columns = []
204
+ for field in table.fields.values():
205
+ kind, length, precision, scale = column_type(field)
206
+ columns.append({"column": self.names(field.name), "type": kind, "len": length,
207
+ "precision": precision, "scale": scale, "nullable": field.nullable})
208
+ out.append((self.names(raw), columns, table.count()))
209
+ return out
210
+
211
+ def rows(self, table, columns, chunk):
212
+ """Batches of tuples, in `columns` order (the MS SQL spelling)."""
213
+ t = self._table(table)
214
+ by_name = dict((self.names(f), f) for f in t.fields)
215
+ raw = [by_name[c] for c in columns]
216
+ fix = [canonical(t.fields[r]) for r in raw]
217
+ batch = []
218
+ for row in t.rows(raw):
219
+ batch.append(tuple(f(v) if f else v for f, v in zip(fix, row)))
220
+ if len(batch) >= chunk:
221
+ yield batch
222
+ batch = []
223
+ if batch:
224
+ yield batch
225
+
226
+ def horizon(self):
227
+ return None
@@ -0,0 +1,158 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ A server infobase on MS SQL as an export source. The only module in the library that
4
+ speaks T-SQL to 1C; everything past the export reads `out/` (see `store.py`).
5
+
6
+ Credentials come from env DF1C_USER / DF1C_PASS; without them, Windows auth is used.
7
+
8
+ The export takes 1C's horizon - `MIN_ACTIVE_ROWVERSION()` - before it reads a table, so a
9
+ delta read off the next export picks up everything this one may have missed.
10
+ """
11
+ import os
12
+ import sys
13
+
14
+ import dump1c
15
+
16
+ PREFERRED_DRIVERS = (
17
+ "ODBC Driver 18 for SQL Server",
18
+ "ODBC Driver 17 for SQL Server",
19
+ "ODBC Driver 13 for SQL Server",
20
+ "SQL Server Native Client 11.0",
21
+ "SQL Server",
22
+ )
23
+
24
+ # A server infobase created without the offset option still has the table; one that has
25
+ # lost it is old enough to have been created with the offset.
26
+ DEFAULT_YEAR_OFFSET = 2000
27
+
28
+
29
+ def connect(server, db):
30
+ import pyodbc
31
+ available = set(pyodbc.drivers())
32
+ driver = next((d for d in PREFERRED_DRIVERS if d in available), None)
33
+ if driver is None:
34
+ raise RuntimeError("no SQL Server ODBC driver found: %r" % sorted(available))
35
+ parts = ["DRIVER={%s}" % driver, "SERVER=%s" % server, "DATABASE=%s" % db,
36
+ "TrustServerCertificate=yes"]
37
+ user, pwd = os.environ.get("DF1C_USER"), os.environ.get("DF1C_PASS")
38
+ if user:
39
+ parts += ["UID=%s" % user, "PWD=%s" % pwd]
40
+ else:
41
+ parts += ["Trusted_Connection=yes"]
42
+ sys.stderr.write("[i] %s / %s via %s\n" % (server, db, driver))
43
+ return pyodbc.connect(";".join(parts), autocommit=True)
44
+
45
+
46
+ class MssqlSource(object):
47
+ kind = "mssql"
48
+ consistent = "per table: each is read after its horizon is taken"
49
+
50
+ def __init__(self, server, db):
51
+ self.server, self.db = server, db
52
+ self.conn = connect(server, db)
53
+ self.cur = self.conn.cursor()
54
+ self._partno = {}
55
+ self._offset = None
56
+
57
+ def describe(self):
58
+ return {"kind": self.kind, "server": self.server, "database": self.db}
59
+
60
+ @property
61
+ def year_offset(self):
62
+ if self._offset is None:
63
+ try:
64
+ self.cur.execute("SELECT Offset FROM _YearOffset")
65
+ row = self.cur.fetchone()
66
+ self._offset = int(row[0]) if row else DEFAULT_YEAR_OFFSET
67
+ except Exception:
68
+ sys.stderr.write("[!] no _YearOffset; assuming %d\n" % DEFAULT_YEAR_OFFSET)
69
+ self._offset = DEFAULT_YEAR_OFFSET
70
+ return self._offset
71
+
72
+ def has_partno(self, table):
73
+ """8.3 splits large files across rows by PartNo; 8.2 stores one row per file."""
74
+ if table not in self._partno:
75
+ self.cur.execute("SELECT COUNT(*) FROM sys.columns WHERE object_id = OBJECT_ID(?)"
76
+ " AND name = 'PartNo'", table)
77
+ self._partno[table] = self.cur.fetchone()[0] > 0
78
+ return self._partno[table]
79
+
80
+ def files(self, table):
81
+ self.cur.execute("SELECT DISTINCT FileName FROM %s" % table)
82
+ return sorted(r[0] for r in self.cur.fetchall())
83
+
84
+ def read_file(self, table, name):
85
+ """Concatenate a file's parts in order, then inflate."""
86
+ order = " ORDER BY PartNo" if self.has_partno(table) else ""
87
+ self.cur.execute("SELECT BinaryData FROM %s WHERE FileName = ?%s" % (table, order), name)
88
+ blob = b"".join(bytes(r[0]) for r in self.cur.fetchall() if r[0] is not None)
89
+ return dump1c.inflate(blob)
90
+
91
+ def tables(self):
92
+ self.cur.execute("""SELECT t.name, c.name, ty.name, c.max_length, c.precision, c.scale, c.is_nullable
93
+ FROM sys.tables t
94
+ JOIN sys.columns c ON c.object_id = t.object_id
95
+ JOIN sys.types ty ON ty.user_type_id = c.user_type_id
96
+ ORDER BY t.name, c.column_id""")
97
+ tables = {}
98
+ for tn, cn, ty, ml, pr, sc, nullable in self.cur.fetchall():
99
+ tables.setdefault(tn, []).append({
100
+ "column": cn, "type": ty, "len": ml,
101
+ "precision": pr, "scale": sc, "nullable": bool(nullable),
102
+ })
103
+ self.cur.execute("""SELECT t.name, SUM(p.rows) FROM sys.tables t
104
+ JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)
105
+ GROUP BY t.name""")
106
+ rows = dict(self.cur.fetchall())
107
+ return [(name, cols, rows.get(name)) for name, cols in sorted(tables.items())]
108
+
109
+ def rows(self, table, columns, chunk, where=None):
110
+ """Batches of rows; `where` is a T-SQL condition on the table's own columns."""
111
+ cur = self.conn.cursor()
112
+ cur.execute("SELECT %s FROM [%s]%s" % (", ".join("[%s]" % c for c in columns), table,
113
+ " WHERE " + where if where else ""))
114
+ while True:
115
+ batch = cur.fetchmany(chunk)
116
+ if not batch:
117
+ break
118
+ yield [tuple(r) for r in batch]
119
+
120
+ def column(self, sql):
121
+ """The first column of a query, every row."""
122
+ cur = self.conn.cursor()
123
+ cur.execute(sql)
124
+ out = []
125
+ while True:
126
+ batch = cur.fetchmany(100000)
127
+ if not batch:
128
+ return out
129
+ out.extend(r[0] for r in batch)
130
+
131
+ def triples(self, sql):
132
+ """The first three columns of a query, every row."""
133
+ cur = self.conn.cursor()
134
+ cur.execute(sql)
135
+ return [(r[0], r[1], r[2]) for r in cur.fetchall()]
136
+
137
+ def horizon(self):
138
+ """Below it every transaction had committed: a read after it misses nothing older."""
139
+ self.cur.execute("SELECT MIN_ACTIVE_ROWVERSION()")
140
+ return bytes(self.cur.fetchone()[0])
141
+
142
+ # ------------------------------------------------------------------- diagnostics
143
+
144
+ def databases(self):
145
+ """Which databases does this login actually see? Separates a bad password from missing rights."""
146
+ self.cur.execute("SELECT name, state_desc, HAS_DBACCESS(name) FROM sys.databases ORDER BY name")
147
+ return self.cur.fetchall()
148
+
149
+ def probe(self):
150
+ self.cur.execute("SELECT @@VERSION")
151
+ lines = [self.cur.fetchone()[0].splitlines()[0]]
152
+ for table in ("Config", "ConfigSave", "Params", "DBSchema", "_YearOffset"):
153
+ try:
154
+ self.cur.execute("SELECT COUNT(*) FROM [%s]" % table)
155
+ lines.append(" %-12s rows=%s" % (table, self.cur.fetchone()[0]))
156
+ except Exception as exc:
157
+ lines.append(" %-12s -- %s" % (table, str(exc).split("]")[-1].strip()[:70]))
158
+ return lines