@avytheone/efmesh 0.2.0 → 0.2.1

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.
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Категоризация изменения по каноническим AST (SPEC §5.2): non-breaking
3
- * это добавление колонок В КОНЕЦ верхнего SELECT при полностью неизменном
4
- * остальном дереве (потребители выбирают по именам, INSERT потомков по
5
- * позициям, поэтому только суффикс безопасен). Всё прочее breaking.
6
- * При любой неожиданной структуре консервативно breaking.
2
+ * Change categorization by canonical ASTs (SPEC §5.2): non-breaking is adding
3
+ * columns TO THE END of the top SELECT with the rest of the tree entirely
4
+ * unchanged (consumers select by name, descendants' INSERT is by position, so
5
+ * only a suffix is safe). Everything else is breaking. On any unexpected
6
+ * structureconservatively breaking.
7
7
  */
8
8
 
9
9
  type AstNode = Record<string, unknown>
10
10
 
11
- /** Верхний SELECT канона: список колонок + остальное дерево (для explain.ts). */
11
+ /** The canon's top SELECT: the column list + the rest of the tree (for explain.ts). */
12
12
  export const topSelect = (
13
13
  astJson: string,
14
14
  ): { list: ReadonlyArray<string>; rest: string } | null => {
@@ -37,7 +37,7 @@ export const categorizeAstChange = (
37
37
  if (before === null || after === null) return "breaking"
38
38
  if (before.rest !== after.rest) return "breaking"
39
39
  if (after.list.length <= before.list.length) return "breaking"
40
- // старый select_list точный префикс нового
40
+ // the old select_list is an exact prefix of the new one
41
41
  return before.list.every((item, index) => item === after.list[index])
42
42
  ? "non-breaking"
43
43
  : "breaking"
@@ -3,24 +3,28 @@ import type { AnyModel } from "../core/model.ts"
3
3
  import type { Engine, EngineError } from "../engine/adapter.ts"
4
4
 
5
5
  /**
6
- * Контракт схемы (SPEC §3.2): объявленная Schema не документация.
7
- * Перед сборкой снапшота efmesh делает DESCRIBE запроса (движок отдаёт
8
- * имена и типы, не выполняя его) и сверяет с объявлением. Дрейф типов
9
- * ловится до бэкфилла, а не после.
6
+ * Schema contract (SPEC §3.2): the declared Schema is not documentation.
7
+ * Before building a snapshot efmesh runs a DESCRIBE of the query (the engine
8
+ * returns names and types without executing it) and checks it against the
9
+ * declaration. Type drift is caught before the backfill, not after.
10
10
  *
11
- * Сверка по семействам типов: точное соответствие TS-типов типам движка
12
- * невозможно (Number это и INTEGER, и DOUBLE), а имена проверяются
13
- * строго. Nullability DESCRIBE не отдаётэто территория аудитов (§14.2).
11
+ * Checked by type families: an exact match of TS types to engine types is
12
+ * impossible (Number is both INTEGER and DOUBLE), while names are checked
13
+ * strictly. DESCRIBE does not report nullability that is audit territory (§14.2).
14
14
  */
15
15
 
16
16
  export class SchemaMismatchError extends Data.TaggedError("SchemaMismatchError")<{
17
17
  readonly model: string
18
18
  readonly problems: ReadonlyArray<string>
19
- }> {}
19
+ }> {
20
+ override get message(): string {
21
+ return `schema contract of model «${this.model}»: ${this.problems.join("; ")}`
22
+ }
23
+ }
20
24
 
21
25
  export type TypeFamily = "text" | "numeric" | "boolean" | "temporal" | "any"
22
26
 
23
- /** Семейство, ожидаемое от поля Effect Schema (по AST). Реюз: testModel рендерит фикстуры по нему. */
27
+ /** Family expected from an Effect Schema field (by AST). Reuse: testModel renders fixtures from it. */
24
28
  export const familyOfAst = (ast: unknown): TypeFamily => {
25
29
  const node = ast as {
26
30
  readonly _tag: string
@@ -40,7 +44,7 @@ export const familyOfAst = (ast: unknown): TypeFamily => {
40
44
  return constructorTag.startsWith("effect/DateTime") ? "temporal" : "any"
41
45
  }
42
46
  case "Union": {
43
- // NullOr(X) и подобные: единственное распознанное семейство среди членов
47
+ // NullOr(X) and the like: the single recognized family among the members
44
48
  const families = new Set(
45
49
  (node.types ?? [])
46
50
  .map(familyOfAst)
@@ -53,7 +57,7 @@ export const familyOfAst = (ast: unknown): TypeFamily => {
53
57
  }
54
58
  }
55
59
 
56
- /** Семейство фактического типа DuckDB из DESCRIBE. */
60
+ /** Family of an actual DuckDB type from DESCRIBE. */
57
61
  const familyOfEngineType = (engineType: string): TypeFamily => {
58
62
  const base = engineType.toUpperCase()
59
63
  if (base === "VARCHAR" || base.startsWith("VARCHAR(")) return "text"
@@ -69,11 +73,11 @@ const familyOfEngineType = (engineType: string): TypeFamily => {
69
73
  }
70
74
 
71
75
  /**
72
- * Сверяет объявленную схему модели с фактическим результатом запроса.
73
- * `renderedSql` — исполнимый рендер тела (ссылки уже в физике/источниках).
74
- * `managed` — колонки, которые efmesh ведёт сам (scdType2: valid_from/
75
- * valid_to): в схеме объявлены потребители их видят, но запрос
76
- * их не возвращает.
76
+ * Checks the model's declared schema against the query's actual result.
77
+ * `renderedSql` — the executable render of the body (references already point
78
+ * at physical storage/sources). `managed` — columns efmesh maintains itself
79
+ * (scdType2: valid_from/valid_to): declared in the schemaconsumers see
80
+ * them but the query does not return them.
77
81
  */
78
82
  export const checkContract = (
79
83
  engine: Engine,
@@ -92,19 +96,19 @@ export const checkContract = (
92
96
  for (const [name, field] of declared) {
93
97
  const engineType = actualByName.get(name)
94
98
  if (engineType === undefined) {
95
- problems.push(`колонка «${name}» объявлена в схеме, но запрос её не возвращает`)
99
+ problems.push(`column «${name}» is declared in the schema but the query does not return it`)
96
100
  continue
97
101
  }
98
102
  const expected = familyOfAst(field.ast)
99
103
  const got = familyOfEngineType(engineType)
100
104
  if (expected !== "any" && got !== "any" && expected !== got) {
101
- problems.push(`колонка «${name}»: схема ждёт ${expected}, запрос отдаёт ${engineType}`)
105
+ problems.push(`column «${name}»: schema expects ${expected}, query returns ${engineType}`)
102
106
  }
103
107
  }
104
108
  const declaredNames = new Set(declared.map(([name]) => name))
105
109
  for (const column of actual) {
106
110
  if (!declaredNames.has(column.name)) {
107
- problems.push(`запрос возвращает колонку «${column.name}», которой нет в схеме`)
111
+ problems.push(`query returns column «${column.name}» which is not in the schema`)
108
112
  }
109
113
  }
110
114
 
package/src/plan/diff.ts CHANGED
@@ -9,12 +9,12 @@ import { viewRef } from "./naming.ts"
9
9
  import { StateStore } from "../state/store.ts"
10
10
  import type { StateError } from "../state/store.ts"
11
11
 
12
- /** Чем окружения отличаются (SPEC §11: `efmesh diff <envA> <envB>`). */
12
+ /** How two environments differ (SPEC §11: `efmesh diff <envA> <envB>`). */
13
13
  export interface EnvDiff {
14
- /** Модель есть только в A. */
14
+ /** Model present only in A. */
15
15
  readonly onlyInA: ReadonlyArray<string>
16
16
  readonly onlyInB: ReadonlyArray<string>
17
- /** Разные версии: имя + fp8 обеих сторон. */
17
+ /** Differing versions: name + fp8 of both sides. */
18
18
  readonly different: ReadonlyArray<{ readonly name: string; readonly a: string; readonly b: string }>
19
19
  readonly same: ReadonlyArray<string>
20
20
  }
@@ -43,38 +43,38 @@ export const diffEnvironments = (
43
43
  })
44
44
 
45
45
  /**
46
- * Сравнение ДАННЫХ двух окружений (#6, класс table_diff sqlmesh): счётчики
47
- * строк, пересечение по ключу, помодельные доли расхождений по колонкам
48
- * между view-слоями A и B одной базы. На DuckDB-классе данных это дёшево;
49
- * для больших таблицдетерминированная выборка по md5 ключа: обе стороны
50
- * фильтруются одинаковыми бакетами, поэтому выборка выровнена и пары
51
- * ключей не теряются.
46
+ * DATA comparison of two environments (#6, sqlmesh's table_diff class): row
47
+ * counts, intersection by key, per-model column-mismatch rates between the
48
+ * A and B view-layers of one database. On DuckDB-class data this is cheap;
49
+ * for large tablesa deterministic sample by md5 of the key: both sides are
50
+ * filtered by the same buckets, so the sample is aligned and key pairs are
51
+ * not lost.
52
52
  */
53
53
 
54
54
  export interface ColumnDrift {
55
55
  readonly column: string
56
- /** Сколько сопоставленных ключей разошлись в этой колонке. */
56
+ /** How many matched keys diverged in this column. */
57
57
  readonly mismatches: number
58
- /** Доля от matched, 0..1. */
58
+ /** Fraction of matched, 0..1. */
59
59
  readonly rate: number
60
60
  }
61
61
 
62
62
  export interface ModelDataDiff {
63
63
  readonly model: string
64
- /** Полные счётчики строк (не задеты выборкой). */
64
+ /** Full row counts (not affected by sampling). */
65
65
  readonly rowsA: number
66
66
  readonly rowsB: number
67
- /** Ключ сопоставления: grain или ключ вида (uniqueKey/scdType2). */
67
+ /** Matching key: grain or a kind's key (uniqueKey/scdType2). */
68
68
  readonly key?: ReadonlyArray<string>
69
- /** Дальшетолько при ключе; при выборке счётчики по выбранным бакетам. */
69
+ /** Belowonly when there is a key; under sampling, counts over selected buckets. */
70
70
  readonly onlyInA?: number
71
71
  readonly onlyInB?: number
72
72
  readonly matched?: number
73
73
  readonly columns?: ReadonlyArray<ColumnDrift>
74
- /** Колонки только одной стороныдрейф схемы между окружениями. */
74
+ /** Columns present on only one side schema drift between environments. */
75
75
  readonly columnsOnlyInA?: ReadonlyArray<string>
76
76
  readonly columnsOnlyInB?: ReadonlyArray<string>
77
- /** Процент md5-бакетов ключа в сравнении; нет поля сравнение полное. */
77
+ /** Percentage of the key's md5 buckets compared; no fieldfull comparison. */
78
78
  readonly sampledPercent?: number
79
79
  }
80
80
 
@@ -85,18 +85,22 @@ export interface DataDiffReport {
85
85
  }
86
86
 
87
87
  export interface DataDiffOptions {
88
- /** Только эти модели; по умолчаниювсе материализуемые из обоих окружений. */
88
+ /** Only these models; by defaultall materializable ones from both environments. */
89
89
  readonly models?: ReadonlyArray<string>
90
- /** 1–99: сравнивать долю ключей (md5-бакеты, выровнено между сторонами). */
90
+ /** 1–99: compare a fraction of keys (md5 buckets, aligned across sides). */
91
91
  readonly samplePercent?: number
92
92
  }
93
93
 
94
94
  export class DataDiffError extends Data.TaggedError("DataDiffError")<{
95
95
  readonly model: string
96
96
  readonly reason: string
97
- }> {}
97
+ }> {
98
+ override get message(): string {
99
+ return `data diff of model «${this.model}»: ${this.reason}`
100
+ }
101
+ }
98
102
 
99
- /** Виды с view-слоем в окруженииих данные есть с чем сравнивать. */
103
+ /** Kinds with a view-layer in the environment their data is comparable. */
100
104
  const COMPARABLE_KINDS: ReadonlySet<string> = new Set([
101
105
  "full",
102
106
  "view",
@@ -115,9 +119,9 @@ const keyOf = (model: AnyModel): ReadonlyArray<string> | undefined => {
115
119
  }
116
120
 
117
121
  /**
118
- * Детерминированный фильтр выборки: md5 от склейки ключа, первые два
119
- * hex-символа = 256 бакетов. Работает и на DuckDB, и на Postgres,
120
- * одинаково на обеих сторонах diff'а.
122
+ * Deterministic sampling filter: md5 of the concatenated key, first two
123
+ * hex chars = 256 buckets. Works on both DuckDB and Postgres, identically
124
+ * on both sides of the diff.
121
125
  */
122
126
  const samplePredicate = (key: ReadonlyArray<string>, percent: number): string => {
123
127
  const buckets = Math.max(1, Math.min(255, Math.floor((256 * percent) / 100)))
@@ -145,12 +149,12 @@ export const dataDiffEnvironments = (
145
149
  const inB = new Set((yield* store.getEnvironment(envB)).map((row) => row.name))
146
150
  for (const name of options?.models ?? []) {
147
151
  if (!graph.models.has(name)) {
148
- return yield* new DataDiffError({ model: name, reason: "модели нет в проекте" })
152
+ return yield* new DataDiffError({ model: name, reason: "model is not in the project" })
149
153
  }
150
154
  if (!inA.has(name) || !inB.has(name)) {
151
155
  return yield* new DataDiffError({
152
156
  model: name,
153
- reason: `модели нет в окружении ${inA.has(name) ? envB : envA}`,
157
+ reason: `model is not in environment "${inA.has(name) ? envB : envA}"`,
154
158
  })
155
159
  }
156
160
  }
@@ -173,8 +177,8 @@ export const dataDiffEnvironments = (
173
177
  (yield* engine.query(`SELECT count(*) AS n FROM ${refB}`))[0]?.["n"],
174
178
  )
175
179
 
176
- // реальные колонки обеих сторон: окружения могут указывать на разные
177
- // версии моделисравниваются только общие, дрейф схемы фиксируется
180
+ // actual columns of both sides: environments may point at different
181
+ // model versionsonly common ones are compared, schema drift is recorded
178
182
  const colsA = (yield* engine.describe(`SELECT * FROM ${refA}`)).map((c) => c.name)
179
183
  const colsB = (yield* engine.describe(`SELECT * FROM ${refB}`)).map((c) => c.name)
180
184
  const setB = new Set(colsB)
@@ -186,8 +190,8 @@ export const dataDiffEnvironments = (
186
190
  const key = keyOf(model)
187
191
  const commonSet = new Set(common)
188
192
  if (key === undefined || !key.every((column) => commonSet.has(column))) {
189
- // сопоставлять нечем (нет grain/ключа или ключ не на обеих сторонах) —
190
- // честные счётчики строк без пары
193
+ // nothing to match on (no grain/key, or the key is not on both sides) —
194
+ // honest row counts without pairing
191
195
  reports.push({
192
196
  model: name,
193
197
  rowsA,