@avytheone/efmesh 0.1.0-beta.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,96 @@
1
+ # Changelog
2
+
3
+ Формат — [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
4
+ версионирование — [SemVer](https://semver.org/lang/ru/).
5
+ Внутренняя история разработки велась фазами F0–F6 (SPEC.md §13);
6
+ первая версия собирает их целиком.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0-beta.1] — 2026-07-16
11
+
12
+ Первая публичная бета. Всё ниже — новое.
13
+
14
+ ### Модели и план (F0–F1)
15
+
16
+ - `defineModel`/`defineExternal`: модели — TypeScript-модули, ссылки —
17
+ импорты, форма данных — Effect Schema; DAG строится из значений.
18
+ - Fingerprint по каноническому AST (`json_serialize_sql` DuckDB):
19
+ переформатирование SQL не триггерит пересборку.
20
+ - Снапшоты версий, виртуальные окружения (view поверх физики),
21
+ план как diff, промоушен без пересчёта.
22
+ - `kind.incrementalByTimeRange`: интервальный учёт в state store, бэкфилл
23
+ батчами DELETE+INSERT в транзакции, resume с места остановки, `lookback`.
24
+ - Контракт схемы перед сборкой (`DESCRIBE` против объявленной Schema).
25
+ - `target: "parquet"`: озеро локально или на S3 (httpfs), интервал = партиция.
26
+
27
+ ### Качество и эксплуатация (F2)
28
+
29
+ - Аудиты `notNull`/`unique`/`accepted`, blocking/warn.
30
+ - `testModel` (`efmesh/testing`): юнит-тесты моделей на фикстурах
31
+ в in-memory DuckDB.
32
+ - Категоризация изменений breaking / non-breaking / indirect по AST.
33
+ - `efmesh run`: идемпотентный тик планировщика с межпроцессным локом;
34
+ `Runner.daemon` для встраивания.
35
+ - `efmesh janitor`, `efmesh diff`, `defineSeed`,
36
+ `kind.incrementalByUniqueKey` (upsert), экспорт витрин в ATTACH-базы,
37
+ метрики и спаны.
38
+
39
+ ### Широта (F3)
40
+
41
+ - Postgres: движок на `Bun.SQL` (пул, параллельные батчи бэкфилла),
42
+ canonicalize через libpg_query, state store в схеме `efmesh_state`.
43
+ - `--forward-only`: правка без переигрывания истории — новая версия
44
+ наследует физику и done-интервалы, новые колонки через `ALTER`.
45
+ - `kind.scdType2` (история строк), `kind.embedded` (подзапрос без
46
+ материализации), `defineSqlModel` (сырые `.sql` с `@ref`/`@start`/`@end`).
47
+ - Колоночный `efmesh lineage`, `efmesh graph --html`.
48
+
49
+ ### Эксплуатационная зрелость (F4)
50
+
51
+ - Межмодельная DAG-конкурентность apply (`--jobs`): модель стартует
52
+ по готовности родителей; на DuckDB честно 1 (одно соединение).
53
+ - `target: "ducklake"`: физика в DuckLake-каталоге, снапшоты и time travel
54
+ каталога — бонус.
55
+ - Автономный `efmesh audit <env>` по view-слою окружения.
56
+ - `efmesh init` (скаффолд), версия схемы state store + `efmesh migrate`.
57
+ - Подтверждение плана в TTY (`--yes` пропускает, не-TTY едет без вопроса).
58
+
59
+ ### Бета-гейт (F5)
60
+
61
+ - Межпроцессный лок на `apply` — общий env-лок с `run`: параллельные
62
+ мутации окружения из разных процессов взаимно исключаются
63
+ (`LockHeldError`); у janitor свой глобальный лок.
64
+ - Discovery моделей по glob: `discovery: "models/**/*.ts"` в конфиге.
65
+ - Ретраи батча бэкфилла: `--retries N`, `Schedule.exponential`;
66
+ аудиты не ретраятся.
67
+ - `applied_by` в журнале планов (версия схемы стора 2).
68
+ - Лицензия MIT.
69
+
70
+ ### Бета-гейт, часть 2 (F6)
71
+
72
+ - effect — peerDependency с точным пином беты; еженедельный CI против
73
+ свежей беты (ранний сигнал дрейфа v4).
74
+ - Fingerprint как контракт: `FINGERPRINT_VERSION` в снапшотах (схема
75
+ стора 3), golden-тесты замораживают канонизацию DuckDB и libpg_query;
76
+ снапшот чужой версии — громкая `FingerprintVersionError`.
77
+ - Гонка janitor↔apply закрыта транзакционно: claim сироты атомарен
78
+ с проверками, воскрешение снимает сиротство, промоушен проверяет
79
+ живость снапшотов — view не переключается на снесённую физику.
80
+ - Parquet-партиции пишутся атомарно (temp + rename); `migrate` снимает
81
+ копию SQLite-стора перед апгрейдом схемы.
82
+ - **Ломающее:** `apply` в не-TTY с изменениями требует `--yes`;
83
+ exit-код `2` = «ждёт человека» (отказ подтверждения,
84
+ `RunBlockedByChangesError`).
85
+ - Публичное API сужено до осознанного whitelist (`index.ts`).
86
+ - README на английском (русское зеркало — README.ru.md), секция
87
+ «для кого это (и для кого нет)».
88
+
89
+ ### Известные ограничения
90
+
91
+ - Effect v4 — beta-зависимость; API efmesh держится стабильного
92
+ подмножества.
93
+ - Одиночный бинарник `bun build --compile` собирается, но standalone-Bun
94
+ не резолвит импорт `"efmesh"` из рантайм-конфига — дистрибуция пакетом.
95
+ - Nullability не входит в контракт схемы (DuckDB `DESCRIBE` её не отдаёт) —
96
+ выражается аудитом `notNull`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexey Yakimanskiy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # efmesh
2
+
3
+ > Data transformation in the spirit of [sqlmesh](https://sqlmesh.com) — on TypeScript, [Bun](https://bun.sh) and [Effect](https://effect.website).
4
+
5
+ [![ci](https://github.com/avytheone/efmesh/actions/workflows/ci.yml/badge.svg)](https://github.com/avytheone/efmesh/actions/workflows/ci.yml) ![status](https://img.shields.io/badge/status-beta-orange) ![version](https://img.shields.io/badge/version-0.1.0--beta.1-blue) ![license](https://img.shields.io/badge/license-MIT-green) ![runtime](https://img.shields.io/badge/runtime-bun-black) ![effect](https://img.shields.io/badge/effect-v4-5C4EE5)
6
+
7
+ *Русская версия: [README.ru.md](./README.ru.md).*
8
+
9
+ Models are plain TypeScript modules: SQL bodies, imports as dependencies, Effect Schema as the data shape. efmesh fingerprints every model by its canonical AST, keeps versions as snapshots, computes a plan as the diff between your project and an environment, and applies exactly that plan: physical tables are rebuilt only where something actually changed, while environments (dev/prod/…) are virtual views over shared physical storage — promoting to prod costs zero recomputation.
10
+
11
+ ```ts
12
+ import { Schema } from "effect"
13
+ import { defineModel, kind } from "@avytheone/efmesh"
14
+ import { rawMoves } from "./sources.ts"
15
+
16
+ export const moves = defineModel(
17
+ {
18
+ name: "med.moves",
19
+ kind: kind.incrementalByTimeRange({
20
+ timeColumn: "moved_at",
21
+ start: "2026-01-01T00:00:00Z",
22
+ lookback: 1, // the tail is re-read — late-arriving data catches up
23
+ }),
24
+ schema: Schema.Struct({ case_id: Schema.String, moved_at: Schema.DateTimeUtc }),
25
+ },
26
+ (ctx) => ctx.sql`
27
+ SELECT ${ctx.cols(rawMoves, "case_id", "moved_at")}
28
+ FROM ${ctx.ref(rawMoves)}
29
+ WHERE moved_at >= ${ctx.start} AND moved_at < ${ctx.end}
30
+ `,
31
+ )
32
+ ```
33
+
34
+ ## Why not dbt / sqlmesh
35
+
36
+ | | dbt | sqlmesh | efmesh |
37
+ |---------------------|-----------------------|------------------------|--------|
38
+ | Model language | SQL + Jinja | SQL + Jinja/Python | SQL inside TypeScript |
39
+ | Dependencies | `ref('string')` | SQL parsing | module imports — checked by the compiler |
40
+ | Column typing | no | contracts (runtime) | Effect Schema: compile-time + a contract before every build |
41
+ | Versioning | none (state-less) | snapshots + fingerprint | snapshots + fingerprint |
42
+ | Dev environments | table copies | virtual (views) | virtual (views) |
43
+ | Incrementality | hand-rolled `is_incremental()` | intervals, tracked | intervals, tracked, resumable |
44
+ | Parquet lake | adapters | adapters | native: `target: "parquet"`, interval = partition |
45
+ | Multi-dialect | yes | yes (sqlglot) | **no** — your engine's dialect (DuckDB or Postgres) |
46
+
47
+ A typo in a `ref` is a compile error, not an empty run; renaming a parent's column breaks the child's build before any SQL reaches the database.
48
+
49
+ ## Who this is for (and who it isn't)
50
+
51
+ **For you**, if you are a TypeScript team on Bun, want a typed dbt/sqlmesh-style workflow on top of DuckDB or Postgres, and are fine living on a beta (efmesh is 0.1.x; Effect v4 is beta, pinned exactly as a peer dependency).
52
+
53
+ **Not for you**, if you need: a Node runtime (Bun-only for now), multi-dialect or cloud DWHs (Snowflake/BigQuery are out of scope), 1.0-grade stability, or the Python ecosystem — take sqlmesh instead, honestly.
54
+
55
+ ## Features
56
+
57
+ **Models.** `full`, `view`, `embedded` (inlined subquery, no materialization), `incrementalByTimeRange` (interval ledger, batched backfill, lookback), `incrementalByUniqueKey` (upsert), `scdType2` (row history, `valid_from`/`valid_to` managed by efmesh), `defineExternal` (tables, parquet/csv/json files, URLs), `defineSeed` (CSV/JSON reference data, content hash in the fingerprint), `defineSqlModel` (raw `.sql` files with `@ref`/`@start`/`@end`).
58
+
59
+ **Materialization targets.** Native engine tables, `parquet` (a lake, local or s3://, interval = partition, views over `read_parquet`), `ducklake` (table-per-fingerprint in a [DuckLake](https://ducklake.select) catalog — catalog snapshots and time travel come as a bonus).
60
+
61
+ **Plans and versions.** Fingerprints over canonical ASTs (reformatting SQL never triggers a rebuild — frozen by golden tests), change categorization breaking / non-breaking / indirect / forward-only, `--forward-only` applies a change without replaying history (the new version inherits physical storage and done-intervals; new columns via `ALTER`), plan confirmation in a TTY, an applied-plans journal with `applied_by`.
62
+
63
+ **Data quality.** A schema contract before every build (`DESCRIBE` of the query against the declared Schema), `notNull` / `unique` / `accepted` audits (blocking fails the apply, `warn` logs), a standalone `efmesh audit` over an environment's view layer, and `testModel` — unit tests for models on fixtures in in-memory DuckDB.
64
+
65
+ **Operations.** `run` — an idempotent scheduler tick for cron/systemd; `apply` and `run` of an environment share one cross-process lock (stale locks of crashed processes are reclaimed by ttl); DAG concurrency `--jobs` (a model starts as soon as its parents are ready); batch retries `--retries`; a janitor for orphaned physical storage (removal is a transactional claim — the race against a concurrent apply is closed); Metric counters and spans on operations; a versioned state-store schema + `efmesh migrate` (with a store file backup).
66
+
67
+ **Engines.** DuckDB (default, including httpfs/ATTACH federation) and Postgres (`Bun.SQL` pool, canonicalization via libpg_query, parallel backfill). State store: SQLite next to the project, or a schema in Postgres.
68
+
69
+ ## Quickstart
70
+
71
+ ```sh
72
+ bun add -d @avytheone/efmesh@beta
73
+ bunx efmesh init my-warehouse && cd my-warehouse
74
+ bunx efmesh plan dev # what would be done
75
+ bunx efmesh apply dev # physical tables, backfill, view layer
76
+ ```
77
+
78
+ `init` scaffolds a working skeleton: `efmesh.config.ts`, example models, a seed. From there, edit models and iterate with `plan`/`apply`; the full lifecycle:
79
+
80
+ ```sh
81
+ bunx efmesh apply dev # apply changes to dev
82
+ bunx efmesh audit dev # audit what the environment serves right now
83
+ bunx efmesh apply prod --yes # promotion: view swap, no recomputation
84
+ bunx efmesh run prod # cron tick: catch up on new intervals
85
+ ```
86
+
87
+ Live example: [examples/hospital](./examples/hospital/) — patient movements across hospital departments, every model kind and target.
88
+
89
+ ## How it works
90
+
91
+ ```
92
+ models (TS modules) ──► DAG + fingerprints over canonical ASTs
93
+
94
+ plan = diff against the state store
95
+
96
+ apply: physical tables ── interval backfill ── audits ── view layer
97
+
98
+ state store: snapshots, intervals, environments, journal
99
+ ```
100
+
101
+ - **Physical layer** — tables `_efmesh.<model>__<fp8>` (or parquet prefixes / DuckLake): a version is a table; the old one lives until the janitor collects it.
102
+ - **Virtual layer** — views `<env>__<schema>.<table>` (prod is just `<schema>.<table>`) pointing at physical storage. An environment is a set of pointers; promotion and rollback are view swaps.
103
+ - **The interval ledger** is the single source of truth about what has been computed: an interrupted backfill resumes where it stopped; recomputing an interval is a transactional DELETE+INSERT of the range — no duplicates.
104
+
105
+ Full architecture, invariants and decisions: [SPEC.md](./SPEC.md) (in Russian).
106
+
107
+ ## Data quality
108
+
109
+ ```ts
110
+ // an audit is a SQL predicate over violations; blocking fails the apply, warn logs
111
+ audits: [
112
+ audit.notNull("case_id"),
113
+ audit.unique("case_id", "moved_at"),
114
+ audit.warn(audit.accepted("dept", ["ICU", "surgery", "therapy"])),
115
+ ]
116
+ ```
117
+
118
+ ```ts
119
+ // a model unit test: fixtures → CTEs → in-memory DuckDB → comparison (bun test)
120
+ import { testModel } from "@avytheone/efmesh/testing"
121
+
122
+ test("stays", () =>
123
+ testModel(stays, {
124
+ inputs: { [moves.name.full]: [{ case_id: "c1", moved_at: "2026-01-01T10:00:00Z" }] },
125
+ expect: [{ case_id: "c1", duration: null }],
126
+ }))
127
+ ```
128
+
129
+ The declared `schema` is a contract, not documentation: before every build efmesh runs `DESCRIBE` on the query and fails with `SchemaMismatchError` if column names or types diverge. NULL guarantees are expressed with the `notNull` audit.
130
+
131
+ ## Configuration
132
+
133
+ `efmesh.config.ts` is a typed TS module — no YAML:
134
+
135
+ ```ts
136
+ import { defineConfig } from "@avytheone/efmesh"
137
+
138
+ export default defineConfig({
139
+ discovery: "models/**/*.ts", // every model export by glob; duplicate names = load error
140
+ // models: [a, b, c], // …or by value (can be combined with discovery)
141
+
142
+ // engine: a DuckDB file by default; Postgres is one line
143
+ engine: { path: "efmesh.duckdb" }, // or { url: "postgres://…", max: 8 }
144
+ state: { path: "efmesh.state.sqlite" }, // or { url: "postgres://…" }
145
+
146
+ lake: { path: "lake" }, // for target: "parquet"; local or s3://
147
+ ducklake: { catalog: "ducklake.sqlite", dataPath: "lake/ducklake" },
148
+ attach: { reporting: { url: "reporting.duckdb" } }, // export targets by alias
149
+ })
150
+ ```
151
+
152
+ ## CLI
153
+
154
+ | Command | What it does |
155
+ |---|---|
156
+ | `efmesh init [dir]` | scaffold a project: config, example models, a seed |
157
+ | `efmesh plan <env>` | diff the project against an environment + missing intervals; changes nothing |
158
+ | `efmesh apply <env>` | plan → confirmation (TTY) → physical tables, backfill, view layer |
159
+ | `efmesh run <env>` | scheduler tick: new intervals only, under the lock; for cron |
160
+ | `efmesh audit <env>` | audit the environment's view layer — catches after-the-fact degradation |
161
+ | `efmesh diff <envA> <envB>` | how two environments differ |
162
+ | `efmesh render <model> [--env]` | the final SQL of a model |
163
+ | `efmesh lineage <model[.col]>` | column lineage down to the raw sources |
164
+ | `efmesh graph [--html]` | the model DAG as text or a page |
165
+ | `efmesh janitor [--ttl 7]` | remove orphaned physical storage older than ttl |
166
+ | `efmesh migrate` | bring the state-store schema up to the current version |
167
+
168
+ `apply`/`run` flags: `--jobs N` — DAG concurrency (always 1 on DuckDB — single connection), `--retries N` — retries for transient batch failures (exponential backoff), `--yes`/`-y` — skip confirmation, `--forward-only <model>,…` — reuse physical storage and history.
169
+
170
+ Exit codes: `0` — success, `1` — error, `2` — "awaiting a human": the plan needs confirmation in a non-TTY (add `--yes`), or `run` hit unapplied changes. In a non-TTY, `apply` with changes and no `--yes` refuses — efmesh will not silently roll out a plan nobody has seen.
171
+
172
+ ## Postgres
173
+
174
+ ```ts
175
+ engine: { url: "postgres://…" }, // canonicalization via libpg_query
176
+ state: { url: "postgres://…" }, // schema efmesh_state
177
+ ```
178
+
179
+ Backfill runs batches in parallel (connection pool); independent DAG branches build concurrently. DuckDB federation (seeds, parquet, external files, export, ducklake) fails honestly on Postgres with `EngineFeatureError` — no silent degradation.
180
+
181
+ ## Status
182
+
183
+ **0.1.0-beta.1.** The core is built and exercised on a live example: phases F0–F6 ([SPEC.md §13](./SPEC.md), [CHANGELOG](./CHANGELOG.md)), 138 tests including a live Postgres cluster and golden tests freezing fingerprint stability. Effect v4 is a beta dependency: pinned exactly (peerDependencies); a weekly CI job tracks drift against fresh betas.
184
+
185
+ Next up: categorization override in the plan dialog, non-time-based intervals. Known limitation: a single `bun build --compile` binary builds, but standalone Bun executables can't resolve the `"efmesh"` import from a runtime-loaded config — distribution is via the package (SPEC §10).
186
+
187
+ ## Documentation
188
+
189
+ - [SPEC.md](./SPEC.md) — the architecture spec: decisions, invariants, open questions (in Russian);
190
+ - [CHANGELOG.md](./CHANGELOG.md) — release history (in Russian);
191
+ - [examples/hospital](./examples/hospital/) — a live example with every model kind;
192
+ - [CONTRIBUTING.md](./CONTRIBUTING.md) — build, test and PR guide (in Russian; PRs and issues in English are welcome).
193
+
194
+ ## License
195
+
196
+ [MIT](./LICENSE) © Alexey Yakimanskiy
package/README.ru.md ADDED
@@ -0,0 +1,203 @@
1
+ # efmesh
2
+
3
+ > Трансформация данных в духе [sqlmesh](https://sqlmesh.com) — на TypeScript, [Bun](https://bun.sh) и [Effect](https://effect.website).
4
+
5
+ *Русское зеркало; основной README — [английский](./README.md).*
6
+
7
+ [![ci](https://github.com/avytheone/efmesh/actions/workflows/ci.yml/badge.svg)](https://github.com/avytheone/efmesh/actions/workflows/ci.yml) ![status](https://img.shields.io/badge/status-beta-orange) ![version](https://img.shields.io/badge/version-0.1.0--beta.1-blue) ![license](https://img.shields.io/badge/license-MIT-green) ![runtime](https://img.shields.io/badge/runtime-bun-black) ![effect](https://img.shields.io/badge/effect-v4-5C4EE5)
8
+
9
+ Модели — обычные TypeScript-модули: тело на SQL, ссылки между моделями — импорты, форма данных — Effect Schema. efmesh считает fingerprint каждой модели по каноническому AST, хранит версии снапшотами, строит план как diff «проект против окружения» и применяет ровно его: физика пересчитывается только там, где что-то реально изменилось, а окружения (dev/prod/…) — виртуальные view поверх общей физики, промоушен в prod не стоит ни одного пересчёта.
10
+
11
+ ```ts
12
+ import { Schema } from "effect"
13
+ import { defineModel, kind } from "@avytheone/efmesh"
14
+ import { rawMoves } from "./sources.ts"
15
+
16
+ export const moves = defineModel(
17
+ {
18
+ name: "med.moves",
19
+ kind: kind.incrementalByTimeRange({
20
+ timeColumn: "moved_at",
21
+ start: "2026-01-01T00:00:00Z",
22
+ lookback: 1, // хвост перечитывается — поздние данные подъезжают
23
+ }),
24
+ schema: Schema.Struct({ case_id: Schema.String, moved_at: Schema.DateTimeUtc }),
25
+ },
26
+ (ctx) => ctx.sql`
27
+ SELECT ${ctx.cols(rawMoves, "case_id", "moved_at")}
28
+ FROM ${ctx.ref(rawMoves)}
29
+ WHERE moved_at >= ${ctx.start} AND moved_at < ${ctx.end}
30
+ `,
31
+ )
32
+ ```
33
+
34
+ ## Почему не dbt / sqlmesh
35
+
36
+ | | dbt | sqlmesh | efmesh |
37
+ |------------------------|-----------------------|-------------------------|--------|
38
+ | Язык моделей | SQL + Jinja | SQL + Jinja/Python | SQL внутри TypeScript |
39
+ | Зависимости | `ref('строка')` | парсинг SQL | импорт модуля — проверяет компилятор |
40
+ | Типизация колонок | нет | contracts (рантайм) | Effect Schema: компайл-тайм + контракт перед сборкой |
41
+ | Версионирование | нет (state-less) | снапшоты + fingerprint | снапшоты + fingerprint |
42
+ | Dev-окружения | копии таблиц | виртуальные (view) | виртуальные (view) |
43
+ | Инкрементальность | самописный `is_incremental()` | интервалы, автоучёт | интервалы, автоучёт, resume |
44
+ | Озеро на parquet | адаптеры | адаптеры | родное: `target: "parquet"`, интервал = партиция |
45
+ | Мульти-диалект | да | да (sqlglot) | **нет** — диалект движка (DuckDB или Postgres) |
46
+
47
+ Опечатка в `ref` у нас — ошибка компиляции, а не пустой прогон; переименованная колонка родителя ломает сборку потомка до того, как SQL уедет в базу.
48
+
49
+ ## Для кого это (и для кого нет)
50
+
51
+ **Для вас**, если вы TypeScript-команда на Bun, хотите типизированный
52
+ dbt/sqlmesh-подход поверх DuckDB или Postgres и готовы жить на бете
53
+ (efmesh — 0.1.x, Effect v4 — beta, версия effect пинована peer-зависимостью).
54
+
55
+ **Не для вас**, если нужны: Node-рантайм (пока только Bun), мульти-диалект
56
+ или облачные DWH (Snowflake/BigQuery — вне scope), стабильность уровня 1.0
57
+ или Python-экосистема — тогда честнее взять sqlmesh.
58
+
59
+ ## Возможности
60
+
61
+ **Модели.** `full`, `view`, `embedded` (подзапрос без материализации), `incrementalByTimeRange` (интервальный учёт, бэкфилл батчами, lookback), `incrementalByUniqueKey` (upsert), `scdType2` (история строк, `valid_from`/`valid_to` ведёт efmesh), `defineExternal` (таблица, файлы parquet/csv/json, URL), `defineSeed` (справочник из CSV/JSON, содержимое в fingerprint), `defineSqlModel` (сырой `.sql`-файл с `@ref`/`@start`/`@end`).
62
+
63
+ **Цели материализации.** Нативная таблица движка, `parquet` (озеро: локально или s3://, интервал = партиция, view поверх `read_parquet`), `ducklake` (таблица-на-fingerprint в [DuckLake](https://ducklake.select)-каталоге — снапшоты и time travel каталога бонусом).
64
+
65
+ **План и версии.** Fingerprint по каноническому AST (переформатирование SQL не триггерит пересборку), категоризация изменений breaking / non-breaking / indirect / forward-only, `--forward-only` — правка без переигрывания истории (новая версия наследует физику и done-интервалы, новые колонки — `ALTER`), подтверждение плана в TTY, журнал применений с `applied_by`.
66
+
67
+ **Качество.** Контракт схемы перед сборкой (`DESCRIBE` запроса против объявленной Schema), аудиты `notNull` / `unique` / `accepted` (blocking роняет apply, `warn` — логирует), автономный `efmesh audit` по view-слою окружения, `testModel` — юнит-тесты моделей на фикстурах в in-memory DuckDB.
68
+
69
+ **Эксплуатация.** `run` — идемпотентный тик планировщика для cron/systemd; `apply` и `run` окружения — под одним межпроцессным локом (протухший лок упавшего процесса перехватывается по ttl); DAG-конкурентность `--jobs` (модель стартует по готовности родителей); ретраи батчей `--retries`; janitor для осиротевшей физики (снос — транзакционный claim, гонка с параллельным apply закрыта); Metric-счётчики и спаны на операциях; версионируемая схема state store + `efmesh migrate` (с бэкапом файла стора).
70
+
71
+ **Движки.** DuckDB (по умолчанию, включая федерацию httpfs/ATTACH) и Postgres (`Bun.SQL`-пул, canonicalize через libpg_query, параллельный бэкфилл). State store — SQLite рядом с проектом или схема в Postgres.
72
+
73
+ ## Быстрый старт
74
+
75
+ ```sh
76
+ bun add -d @avytheone/efmesh@beta
77
+ bunx efmesh init my-warehouse && cd my-warehouse
78
+ bunx efmesh plan dev # что будет сделано
79
+ bunx efmesh apply dev # физика, бэкфилл, view-слой
80
+ ```
81
+
82
+ `init` разворачивает рабочий скелет: `efmesh.config.ts`, модели-пример, seed. Дальше — правьте модели и гоняйте `plan`/`apply`; полный жизненный цикл:
83
+
84
+ ```sh
85
+ bunx efmesh apply dev # применить изменения в dev
86
+ bunx efmesh audit dev # аудиты того, что окружение отдаёт сейчас
87
+ bunx efmesh apply prod --yes # промоушен: view-swap, без пересчёта
88
+ bunx efmesh run prod # cron-тик: догнать новые интервалы
89
+ ```
90
+
91
+ Живой пример: [examples/hospital](./examples/hospital/) — движения пациентов по отделениям, все виды моделей и целей.
92
+
93
+ ## Как это устроено
94
+
95
+ ```
96
+ модели (TS-модули) ──► DAG + fingerprint по каноническому AST
97
+
98
+ план = diff против state store
99
+
100
+ apply: физика ── бэкфилл интервалов ── аудиты ── view-слой
101
+
102
+ state store: снапшоты, интервалы, окружения, журнал
103
+ ```
104
+
105
+ - **Физический слой** — таблицы `_efmesh.<модель>__<fp8>` (или parquet-префиксы/DuckLake): версия = таблица, старая живёт до janitor.
106
+ - **Виртуальный слой** — view `<env>__<schema>.<таблица>` (prod — просто `<schema>.<таблица>`), указывающие на физику. Окружение — это набор указателей; промоушен и откат — переключение view.
107
+ - **Учёт интервалов** — единственный источник правды о посчитанном: упавший бэкфилл продолжается с места остановки, пересчёт интервала — DELETE+INSERT диапазона в транзакции, без дублей.
108
+
109
+ Полная архитектура, инварианты и решения — в [SPEC.md](./SPEC.md).
110
+
111
+ ## Качество данных
112
+
113
+ ```ts
114
+ // аудит: SQL-предикат нарушений; blocking роняет apply, warn — логирует
115
+ audits: [
116
+ audit.notNull("case_id"),
117
+ audit.unique("case_id", "moved_at"),
118
+ audit.warn(audit.accepted("dept", ["КПП", "ОРИТ", "терапия", "хирургия"])),
119
+ ]
120
+ ```
121
+
122
+ ```ts
123
+ // юнит-тест модели: фикстуры → CTE → in-memory DuckDB → сверка (bun test)
124
+ import { testModel } from "@avytheone/efmesh/testing"
125
+
126
+ test("stays", () =>
127
+ testModel(stays, {
128
+ inputs: { [moves.name.full]: [{ case_id: "c1", moved_at: "2026-01-01T10:00:00Z" }] },
129
+ expect: [{ case_id: "c1", duration: null }],
130
+ }))
131
+ ```
132
+
133
+ Объявленная `schema` — не документация, а контракт: перед сборкой efmesh делает `DESCRIBE` запроса и падает с `SchemaMismatchError`, если имена или типы колонок разошлись. NULL-гарантии выражаются аудитом `notNull`.
134
+
135
+ ## Конфигурация
136
+
137
+ `efmesh.config.ts` — типизированный TS-модуль, никакого YAML:
138
+
139
+ ```ts
140
+ import { defineConfig } from "@avytheone/efmesh"
141
+
142
+ export default defineConfig({
143
+ discovery: "models/**/*.ts", // все экспорты-модели по glob; дубликат имени = ошибка
144
+ // models: [a, b, c], // …или значениями (можно совместно с discovery)
145
+
146
+ // движок: DuckDB-файл по умолчанию, Postgres — одной строкой
147
+ engine: { path: "efmesh.duckdb" }, // или { url: "postgres://…", max: 8 }
148
+ state: { path: "efmesh.state.sqlite" }, // или { url: "postgres://…" }
149
+
150
+ lake: { path: "lake" }, // для target: "parquet"; локально или s3://
151
+ ducklake: { catalog: "ducklake.sqlite", dataPath: "lake/ducklake" },
152
+ attach: { reporting: { url: "reporting.duckdb" } }, // export-цели по алиасам
153
+ })
154
+ ```
155
+
156
+ ## CLI
157
+
158
+ | Команда | Что делает |
159
+ |---|---|
160
+ | `efmesh init [dir]` | скаффолд проекта: конфиг, модели-пример, seed |
161
+ | `efmesh plan <env>` | diff проекта против окружения + недостающие интервалы, ничего не меняет |
162
+ | `efmesh apply <env>` | план → подтверждение (TTY) → физика, бэкфилл, view-слой |
163
+ | `efmesh run <env>` | тик планировщика: только новые интервалы, под локом; для cron |
164
+ | `efmesh audit <env>` | аудиты view-слоя окружения — ловит деградацию задним числом |
165
+ | `efmesh diff <envA> <envB>` | чем окружения отличаются |
166
+ | `efmesh render <model> [--env]` | итоговый SQL модели |
167
+ | `efmesh lineage <model[.col]>` | колоночный lineage до сырья |
168
+ | `efmesh graph [--html]` | DAG моделей текстом или страницей |
169
+ | `efmesh janitor [--ttl 7]` | снести осиротевшую физику старше ttl |
170
+ | `efmesh migrate` | догнать схему state store до текущей версии |
171
+
172
+ Флаги `apply`/`run`: `--jobs N` — DAG-конкурентность (на DuckDB всегда 1 — одно соединение), `--retries N` — ретраи транзиентных сбоев батча (экспоненциальная пауза), `--yes`/`-y` — без подтверждения, `--forward-only <model>,…` — реюз физики и истории.
173
+
174
+ Exit-коды: `0` — успех, `1` — ошибка, `2` — «ждёт человека»: план требует
175
+ подтверждения в не-TTY (нужен `--yes`) или `run` упёрся в неприменённые
176
+ изменения. В не-TTY `apply` с изменениями БЕЗ `--yes` не применяется —
177
+ молча катить план, который никто не видел, efmesh отказывается.
178
+
179
+ ## Postgres
180
+
181
+ ```ts
182
+ engine: { url: "postgres://…" }, // canonicalize через libpg_query
183
+ state: { url: "postgres://…" }, // схема efmesh_state
184
+ ```
185
+
186
+ Бэкфилл гонит батчи параллельно (пул соединений), независимые ветки DAG строятся одновременно. DuckDB-федерация (seed, parquet, external-файлы, export, ducklake) на Postgres честно падает `EngineFeatureError` — без тихой деградации.
187
+
188
+ ## Статус
189
+
190
+ **0.1.0-beta.1.** Ядро построено и прогнано на живом примере: фазы F0–F6 (см. [SPEC.md §13](./SPEC.md) и [CHANGELOG](./CHANGELOG.md)), 138 тестов, включая живой Postgres-кластер и golden-тесты стабильности fingerprint. Effect v4 — beta-зависимость: пинована точно (peerDependencies), дрейф свежих бет ловит еженедельный CI.
191
+
192
+ Дальше по спеке: override категоризации в диалоге плана, интервалы не по времени. Известное ограничение: одиночный бинарник `bun build --compile` собирается, но standalone-исполняемые Bun не резолвят импорт `"efmesh"` из рантайм-конфига — дистрибуция пакетом (SPEC §10).
193
+
194
+ ## Документация
195
+
196
+ - [SPEC.md](./SPEC.md) — архитектурная спецификация: решения, инварианты, открытые вопросы;
197
+ - [CHANGELOG.md](./CHANGELOG.md) — история изменений;
198
+ - [examples/hospital](./examples/hospital/) — живой пример со всеми видами моделей;
199
+ - [CONTRIBUTING.md](./CONTRIBUTING.md) — как собрать, погонять тесты и предложить правку.
200
+
201
+ ## Лицензия
202
+
203
+ [MIT](./LICENSE) © Alexey Yakimanskiy