metal-orm 1.1.10 β†’ 1.1.16

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 (192) hide show
  1. package/README.md +758 -769
  2. package/dist/index.cjs +106 -43
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +5 -5
  5. package/dist/index.d.ts +5 -5
  6. package/dist/index.js +106 -43
  7. package/dist/index.js.map +1 -1
  8. package/package.json +100 -96
  9. package/scripts/generate-entities/render.mjs +21 -12
  10. package/scripts/generate-entities/schema.mjs +209 -195
  11. package/scripts/generate-entities/tree-detection.mjs +159 -153
  12. package/scripts/inflection/pt-br.mjs +468 -468
  13. package/scripts/run-eslint.mjs +33 -34
  14. package/scripts/show-sql-seed.sql +102 -0
  15. package/scripts/show-sql.mjs +321 -325
  16. package/src/bulk/bulk-delete-executor.ts +3 -5
  17. package/src/bulk/bulk-insert-executor.ts +7 -7
  18. package/src/bulk/bulk-update-executor.ts +2 -2
  19. package/src/bulk/bulk-upsert-executor.ts +5 -7
  20. package/src/codegen/naming-strategy.ts +54 -54
  21. package/src/codegen/typescript.ts +482 -482
  22. package/src/core/ast/adapters.ts +1 -1
  23. package/src/core/ast/builders.ts +123 -123
  24. package/src/core/ast/expression-builders.ts +745 -745
  25. package/src/core/ast/expression-nodes.ts +304 -304
  26. package/src/core/ast/expression-visitor.ts +212 -212
  27. package/src/core/ast/expression.ts +20 -20
  28. package/src/core/ast/helpers.ts +14 -14
  29. package/src/core/ast/join.ts +18 -18
  30. package/src/core/ast/query.ts +200 -200
  31. package/src/core/ddl/dialects/base-schema-dialect.ts +1 -1
  32. package/src/core/ddl/dialects/mssql-schema-dialect.ts +1 -1
  33. package/src/core/ddl/dialects/mysql-schema-dialect.ts +1 -1
  34. package/src/core/ddl/dialects/postgres-schema-dialect.ts +1 -1
  35. package/src/core/ddl/dialects/render-reference.test.ts +70 -70
  36. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +1 -1
  37. package/src/core/ddl/introspect/catalogs/index.ts +5 -5
  38. package/src/core/ddl/introspect/catalogs/postgres.ts +146 -146
  39. package/src/core/ddl/introspect/context.ts +15 -15
  40. package/src/core/ddl/introspect/functions/postgres.ts +35 -35
  41. package/src/core/ddl/introspect/mysql.ts +113 -36
  42. package/src/core/ddl/introspect/registry.ts +40 -40
  43. package/src/core/ddl/introspect/run-select.ts +35 -35
  44. package/src/core/ddl/introspect/utils.ts +56 -56
  45. package/src/core/ddl/naming-strategy.ts +16 -16
  46. package/src/core/ddl/schema-dialect.ts +56 -56
  47. package/src/core/ddl/schema-diff.ts +234 -234
  48. package/src/core/ddl/schema-generator.ts +1 -1
  49. package/src/core/ddl/schema-introspect.ts +25 -25
  50. package/src/core/ddl/sql-writing.ts +171 -171
  51. package/src/core/dialect/abstract.ts +541 -541
  52. package/src/core/dialect/base/cte-compiler.ts +33 -33
  53. package/src/core/dialect/base/function-table-formatter.ts +103 -103
  54. package/src/core/dialect/base/groupby-compiler.ts +21 -21
  55. package/src/core/dialect/base/join-compiler.ts +25 -25
  56. package/src/core/dialect/base/orderby-compiler.ts +35 -35
  57. package/src/core/dialect/base/pagination-strategy.ts +32 -32
  58. package/src/core/dialect/base/returning-strategy.ts +57 -57
  59. package/src/core/dialect/base/sql-dialect.ts +294 -294
  60. package/src/core/dialect/dialect-factory.ts +91 -91
  61. package/src/core/dialect/mssql/functions.ts +120 -120
  62. package/src/core/dialect/mssql/index.ts +317 -317
  63. package/src/core/dialect/mysql/functions.ts +103 -103
  64. package/src/core/dialect/mysql/index.ts +105 -105
  65. package/src/core/dialect/postgres/functions.ts +108 -108
  66. package/src/core/dialect/postgres/index.ts +91 -91
  67. package/src/core/dialect/sqlite/functions.ts +129 -129
  68. package/src/core/dialect/sqlite/index.ts +67 -67
  69. package/src/core/driver/database-driver.ts +19 -19
  70. package/src/core/driver/mssql-driver.ts +23 -23
  71. package/src/core/driver/mysql-driver.ts +23 -23
  72. package/src/core/driver/postgres-driver.ts +23 -23
  73. package/src/core/driver/sqlite-driver.ts +23 -23
  74. package/src/core/execution/db-executor.ts +92 -92
  75. package/src/core/execution/executors/better-sqlite3-executor.ts +94 -94
  76. package/src/core/execution/executors/mssql-executor.ts +184 -184
  77. package/src/core/execution/executors/mysql-executor.ts +88 -88
  78. package/src/core/execution/executors/postgres-executor.ts +26 -26
  79. package/src/core/execution/executors/sqlite-executor.ts +42 -42
  80. package/src/core/execution/pooling/pool-types.ts +30 -30
  81. package/src/core/execution/pooling/pool.ts +275 -275
  82. package/src/core/functions/array.ts +35 -35
  83. package/src/core/functions/control-flow.ts +79 -79
  84. package/src/core/functions/datetime.ts +237 -237
  85. package/src/core/functions/json.ts +70 -70
  86. package/src/core/functions/numeric.ts +297 -297
  87. package/src/core/functions/text.ts +334 -334
  88. package/src/core/functions/types.ts +33 -33
  89. package/src/core/hydration/types.ts +51 -51
  90. package/src/core/sql/sql-operator-config.ts +39 -39
  91. package/src/core/sql/sql.ts +152 -152
  92. package/src/decorators/column-decorator.ts +87 -87
  93. package/src/decorators/decorator-metadata.ts +143 -143
  94. package/src/decorators/entity.ts +93 -93
  95. package/src/decorators/index.ts +39 -39
  96. package/src/decorators/relations.ts +231 -231
  97. package/src/decorators/transformers/built-in/string-transformers.ts +231 -231
  98. package/src/decorators/transformers/transformer-decorators.ts +134 -134
  99. package/src/decorators/transformers/transformer-executor.ts +171 -171
  100. package/src/decorators/transformers/transformer-metadata.ts +73 -73
  101. package/src/decorators/validators/built-in/br-cep-validator.ts +55 -55
  102. package/src/decorators/validators/built-in/br-cnpj-validator.ts +105 -105
  103. package/src/decorators/validators/built-in/br-cpf-validator.ts +103 -103
  104. package/src/decorators/validators/country-validator-registry.ts +64 -64
  105. package/src/decorators/validators/country-validators-decorators.ts +203 -203
  106. package/src/decorators/validators/country-validators.ts +105 -105
  107. package/src/decorators/validators/validator-metadata.ts +59 -59
  108. package/src/dto/dto-types.ts +226 -226
  109. package/src/dto/filter-types.ts +141 -141
  110. package/src/dto/index.ts +95 -95
  111. package/src/dto/openapi/generators/base.ts +29 -29
  112. package/src/dto/openapi/generators/column.ts +37 -37
  113. package/src/dto/openapi/generators/dto.ts +91 -91
  114. package/src/dto/openapi/generators/filter.ts +75 -75
  115. package/src/dto/openapi/generators/nested-dto.ts +618 -618
  116. package/src/dto/openapi/generators/pagination.ts +111 -111
  117. package/src/dto/openapi/generators/relation-filter.ts +228 -228
  118. package/src/dto/openapi/index.ts +12 -12
  119. package/src/dto/openapi/types.ts +101 -101
  120. package/src/dto/openapi/utilities.ts +95 -95
  121. package/src/dto/pagination-utils.ts +150 -150
  122. package/src/dto/transform.ts +197 -197
  123. package/src/index.ts +86 -86
  124. package/src/orm/als.ts +58 -58
  125. package/src/orm/domain-event-bus.ts +119 -119
  126. package/src/orm/entity-context.ts +86 -86
  127. package/src/orm/entity-materializer.ts +159 -159
  128. package/src/orm/entity-meta.ts +97 -97
  129. package/src/orm/entity-registry.ts +39 -39
  130. package/src/orm/entity-relation-cache.ts +39 -39
  131. package/src/orm/execute.ts +194 -194
  132. package/src/orm/execution-context.ts +18 -18
  133. package/src/orm/hydration-context.ts +26 -26
  134. package/src/orm/hydration.ts +112 -112
  135. package/src/orm/identity-map.ts +61 -61
  136. package/src/orm/interceptor-pipeline.ts +16 -16
  137. package/src/orm/lazy-batch/belongs-to-many.ts +119 -119
  138. package/src/orm/lazy-batch/belongs-to.ts +108 -108
  139. package/src/orm/lazy-batch/has-many.ts +69 -69
  140. package/src/orm/lazy-batch/has-one.ts +68 -68
  141. package/src/orm/lazy-batch/shared.ts +125 -125
  142. package/src/orm/orm-session.ts +620 -620
  143. package/src/orm/orm.ts +34 -34
  144. package/src/orm/pooled-executor-factory.ts +121 -121
  145. package/src/orm/query-logger.ts +40 -40
  146. package/src/orm/relation-change-processor.ts +420 -420
  147. package/src/orm/relations/belongs-to.ts +143 -143
  148. package/src/orm/relations/has-many.ts +204 -204
  149. package/src/orm/relations/has-one.ts +174 -174
  150. package/src/orm/relations/many-to-many.ts +288 -288
  151. package/src/orm/relations/morph-many.ts +156 -156
  152. package/src/orm/relations/morph-one.ts +151 -151
  153. package/src/orm/relations/morph-to.ts +162 -162
  154. package/src/orm/runtime-types.ts +92 -92
  155. package/src/orm/save-graph.ts +561 -561
  156. package/src/orm/transaction-runner.ts +24 -24
  157. package/src/orm/unit-of-work.ts +426 -426
  158. package/src/query/index.ts +69 -69
  159. package/src/query/target.ts +46 -46
  160. package/src/query-builder/column-selector.ts +81 -81
  161. package/src/query-builder/delete.ts +141 -141
  162. package/src/query-builder/insert-query-state.ts +149 -149
  163. package/src/query-builder/insert.ts +117 -117
  164. package/src/query-builder/query-ast-service.ts +287 -287
  165. package/src/query-builder/query-resolution.ts +78 -78
  166. package/src/query-builder/raw-column-parser.ts +38 -38
  167. package/src/query-builder/relation-filter-utils.ts +153 -153
  168. package/src/query-builder/relation-join-planner.ts +10 -10
  169. package/src/query-builder/relation-manager.ts +87 -87
  170. package/src/query-builder/relation-projection-helper.ts +102 -102
  171. package/src/query-builder/relation-types.ts +9 -9
  172. package/src/query-builder/relation-utils.ts +15 -15
  173. package/src/query-builder/select/cte-facet.ts +40 -40
  174. package/src/query-builder/select/from-facet.ts +80 -80
  175. package/src/query-builder/select/join-facet.ts +62 -62
  176. package/src/query-builder/select/predicate-facet.ts +104 -104
  177. package/src/query-builder/select/projection-facet.ts +70 -70
  178. package/src/query-builder/select/relation-facet.ts +81 -81
  179. package/src/query-builder/select/setop-facet.ts +36 -36
  180. package/src/query-builder/select-helpers.ts +1 -1
  181. package/src/query-builder/select-query-builder-deps.ts +130 -130
  182. package/src/query-builder/select-query-state.ts +207 -207
  183. package/src/query-builder/update.ts +151 -151
  184. package/src/schema/column-types.ts +322 -322
  185. package/src/schema/table-guards.ts +37 -37
  186. package/src/schema/table.ts +281 -281
  187. package/src/tree/index.ts +13 -13
  188. package/src/tree/nested-set-strategy.ts +545 -545
  189. package/src/tree/tree-decorator.ts +397 -397
  190. package/src/tree/tree-manager.ts +754 -754
  191. package/src/tree/tree-query.ts +427 -427
  192. package/src/tree/tree-types.ts +299 -299
package/README.md CHANGED
@@ -1,769 +1,758 @@
1
- # MetalORM βš™οΈ
2
-
3
- [![npm version](https://img.shields.io/npm/v/metal-orm.svg)](https://www.npmjs.com/package/metal-orm)
4
- [![license](https://img.shields.io/npm/l/metal-orm.svg)](https://github.com/celsowm/metal-orm/blob/main/LICENSE)
5
- [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%23007ACC.svg)](https://www.typescriptlang.org/)
6
-
7
- > **TypeScript-first ORM that adapts to your needs**: use it as a type-safe query builder, a full-featured ORM runtime, or anything in between.
8
-
9
- ## Why MetalORM? πŸ’‘
10
-
11
- - 🎯 **Gradual adoption**: Start with just SQL building, add ORM features when you need them
12
- - πŸ”’ **Exceptionally strongly typed**: Built with TypeScript generics and type inferenceβ€”**zero** `any` types in the entire codebase
13
- - πŸ—οΈ **Well-architected**: Implements proven design patterns (Strategy, Visitor, Builder, Unit of Work, Identity Map, Interceptor, and more)
14
- - 🎨 **One AST, multiple levels**: All features share the same SQL AST foundationβ€”no magic, just composable layers
15
- - πŸš€ **Multi-dialect from the start**: MySQL, PostgreSQL, SQLite, SQL Server support built-in
16
-
17
- ---
18
-
19
- ## ⚑ 30-Second Quick Start
20
-
21
- ```ts
22
- import { defineTable, col, selectFrom, MySqlDialect } from 'metal-orm';
23
-
24
- const users = defineTable('users', {
25
- id: col.primaryKey(col.int()),
26
- name: col.varchar(255),
27
- });
28
-
29
- const query = selectFrom(users).select('id', 'name').limit(10);
30
- const { sql, params } = query.compile(new MySqlDialect());
31
- // That's it! Use sql + params with any driver.
32
- // ↑ Fully typedβ€”no casting, no 'any', just strong types all the way down
33
- ```
34
-
35
- ---
36
-
37
- ## Three Levels of Abstraction
38
-
39
- MetalORM is a TypeScript-first, AST-driven SQL toolkit you can dial up or down depending on how "ORM-y" you want to be:
40
-
41
- - **Level 1 – Query builder & hydration 🧩**
42
- Define tables with `defineTable` / `col.*`, build strongly-typed queries on a real SQL AST, and hydrate flat result sets into nested objects – no ORM runtime involved.
43
-
44
- - **Level 2 – ORM runtime (entities + Unit of Work 🧠)**
45
- Let `OrmSession` (created from `Orm`) turn rows into tracked entities with lazy relations, cascades, and a [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) that flushes changes with `session.commit()`.
46
-
47
- - **Level 3 – Decorator entities (classes + metadata ✨)**
48
- Use `@Entity`, `@Column`, `@PrimaryKey`, relation decorators, `bootstrapEntities()` (or the lazy bootstrapping in `getTableDefFromEntity` / `selectFromEntity`) to describe your model classes. MetalORM bootstraps schema & relations from metadata and plugs them into the same runtime and query builder.
49
-
50
- **Use only the layer you need in each part of your codebase.**
51
-
52
- ---
53
-
54
- <a id="table-of-contents"></a>
55
- ## Table of Contents 🧭
56
-
57
- - [Documentation](#documentation)
58
- - [Features](#features)
59
- - [Installation](#installation)
60
- - [Quick start - three levels](#quick-start)
61
- - [Level 1 – Query builder & hydration](#level-1)
62
- - [Level 2 – Entities + Unit of Work](#level-2)
63
- - [Level 3 – Decorator entities](#level-3)
64
- - [When to use which level?](#when-to-use-which-level)
65
- - [Design & Architecture](#design-notes)
66
- - [FAQ](#frequently-asked-questions-)
67
- - [Performance & Production](#performance--production-)
68
- - [Community & Support](#community--support-)
69
- - [Contributing](#contributing)
70
- - [License](#license)
71
-
72
- ---
73
-
74
- <a id="documentation"></a>
75
- ## Documentation πŸ“š
76
-
77
- Full docs live in the `docs/` folder:
78
-
79
- - [Introduction](https://github.com/celsowm/metal-orm/blob/main/docs/index.md)
80
- - [Getting Started](https://github.com/celsowm/metal-orm/blob/main/docs/getting-started.md)
81
- - [Level 3 Backend Tutorial](https://github.com/celsowm/metal-orm/blob/main/docs/level-3-backend-tutorial.md)
82
- - [Schema Definition](https://github.com/celsowm/metal-orm/blob/main/docs/schema-definition.md)
83
- - [Query Builder](https://github.com/celsowm/metal-orm/blob/main/docs/query-builder.md)
84
- - [Tree Behavior (Nested Set/MPTT)](https://github.com/celsowm/metal-orm/blob/main/docs/tree.md)
85
- - [DTO (Data Transfer Objects)](https://github.com/celsowm/metal-orm/blob/main/docs/dto.md)
86
- - [OpenAPI Schema Generation](https://github.com/celsowm/metal-orm/blob/main/docs/openapi.md)
87
- - [DML Operations](https://github.com/celsowm/metal-orm/blob/main/docs/dml-operations.md)
88
- - [Hydration & Entities](https://github.com/celsowm/metal-orm/blob/main/docs/hydration.md)
89
- - [Runtime & Unit of Work](https://github.com/celsowm/metal-orm/blob/main/docs/runtime.md)
90
- - [Save Graph](https://github.com/celsowm/metal-orm/blob/main/docs/save-graph.md)
91
- - [Caching](https://github.com/celsowm/metal-orm/blob/main/docs/caching.md)
92
- - [Advanced Features](https://github.com/celsowm/metal-orm/blob/main/docs/advanced-features.md)
93
- - [Multi-Dialect Support](https://github.com/celsowm/metal-orm/blob/main/docs/multi-dialect-support.md)
94
- - [Schema Generation (DDL)](https://github.com/celsowm/metal-orm/blob/main/docs/schema-generation.md)
95
- - [Polymorphic Relations](https://github.com/celsowm/metal-orm/blob/main/docs/relations/14-polymorphic-relations.md)
96
- - [API Reference](https://github.com/celsowm/metal-orm/blob/main/docs/api-reference.md)
97
- - [DB ➜ TS Type Mapping](https://github.com/celsowm/metal-orm/blob/main/docs/db-to-ts-types.md)
98
- - [Stored Procedures](https://github.com/celsowm/metal-orm/blob/main/docs/stored-procedures.md)
99
-
100
- ---
101
-
102
- <a id="features"></a>
103
- ## Features πŸš€
104
-
105
- ### Level 1 – Query builder & hydration
106
-
107
- - **Declarative schema definition** with `defineTable`, `col.*`, and typed relations (including polymorphic `morphOne`, `morphMany`, `morphTo`).
108
- - **Typed temporal columns**: `col.date()` / `col.datetime()` / `col.timestamp()` default to `string` but accept a generic when your driver returns `Date` (e.g. `col.date<Date>()`).
109
- - **Fluent query builder** over a real SQL AST
110
- (`SelectQueryBuilder`, `InsertQueryBuilder`, `UpdateQueryBuilder`, `DeleteQueryBuilder`).
111
- - **Advanced SQL**: CTEs, aggregates, window functions, subqueries, bitwise operators (`&`, `|`, `^`, `<<`, `>>`), JSON, CASE, EXISTS, and the full SQL function catalog (e.g. `STDDEV`, `VARIANCE`, `LOG2`, `CBRT`, `COALESCE`, `NULLIF`, `GREATEST`, `LEAST`, `IFNULL`, `LOCALTIME`, `LOCALTIMESTAMP`, `AGE`).
112
- - **Table-valued functions**: use the new `tvf(key, …)` helper when you want portable intents such as `ARRAY_UNNEST`, letting the dialects’ `TableFunctionStrategy` renderers emit dialect-specific syntax (`LATERAL`/`WITH ORDINALITY`, alias validation, quoting, etc.). `fnTable()` remains available as the raw escape hatch when you need to emit a specific SQL function directly.
113
- - **String helpers**: `lower`, `upper`, `trim`, `ltrim/rtrim`, `concat/concatWs`, `substr/left/right`, `position/instr/locate`, `replace`, `repeat`, `lpad/rpad`, `space`, and more with dialect-aware rendering.
114
- - **Set operations**: `union`, `unionAll`, `intersect`, `except` across all dialects (ORDER/LIMIT apply to the combined result; hydration is disabled for compound queries so rows are returned as-is without collapsing duplicates).
115
- - **Expression builders**: `eq`, `and`, `or`, `not`, `between`, `inList`, `exists`, `jsonPath`, `caseWhen`, window functions like `rowNumber`, `rank`, `lag`, `lead`, etc., all backed by typed AST nodes.
116
- - **Operator safety**: scalar operators (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`) are for single values; for arrays, use `inList`/`notInList`.
117
- - Migration example: `where(eq(tipoAcao.columns.codigo, codigos))` -> `where(inList(tipoAcao.columns.codigo, codigos))`.
118
- - **Relation-aware hydration**: turn flat rows into nested objects (`user.posts`, `user.roles`, etc.) using a hydration plan derived from the AST metadata.
119
- - **Multi-dialect**: compile once, run on MySQL/MariaDB, PostgreSQL, SQLite, or SQL Server via pluggable dialects.
120
- - **DML**: type-safe INSERT / UPDATE / DELETE with `RETURNING` where supported.
121
- - Includes upsert support via `.onConflict(...).doUpdate(...)` / `.doNothing()` with dialect-specific SQL generation.
122
-
123
- Level 1 is ideal when you:
124
-
125
- - Already have a domain model and just want a serious SQL builder.
126
- - Want deterministic SQL (no magical query generation).
127
- - Need to share the same AST across tooling (e.g. codegen, diagnostics, logging).
128
-
129
- ### Level 2 – ORM runtime (`OrmSession`)
130
-
131
- On top of the query builder, MetalORM ships a focused runtime managed by `Orm` and its request-scoped `OrmSession`s:
132
-
133
- - **Entities inferred from your `TableDef`s** (no separate mapping file).
134
- - **Lazy, batched relations**: `user.posts.load()`, `user.roles.syncByIds([...])`, etc.
135
- - **Scoped transactions**: `session.transaction(async s => { ... })` wraps `begin/commit/rollback` and uses savepoints for nested calls on the same session; `Orm.transaction` remains available when you want a fresh transactional executor per call.
136
- - **Identity map**: the same row becomes the same entity instance within a session (see the [Identity map pattern](https://en.wikipedia.org/wiki/Identity_map_pattern)).
137
- - **Caching**: Flexible caching with `MemoryCacheAdapter` (dev), `KeyvCacheAdapter` (simple production), or `RedisCacheAdapter` (full-featured with tag support). Features human-readable TTL (`'30m'`, `'2h'`), tag-based invalidation, and multi-tenant cache isolation.
138
- - **Tree Behavior (Nested Set/MPTT)**: hierarchical data with `TreeManager`, `treeQuery()`, and `@Tree` decorators. Efficient O(log n) operations for moves, inserts, and deletes. Supports multi-tree scoping, recovery, and validation.
139
- - **DTO/OpenAPI helpers**: the `metal-orm/dto` module generates DTOs and OpenAPI schemas, including tree schemas (`TreeNode`, `TreeNodeResult`, threaded trees).
140
- - **Unit of Work (`OrmSession`)** tracking New/Dirty/Removed entities and relation changes, inspired by the classic [Unit of Work pattern](https://en.wikipedia.org/wiki/wiki/Unit_of_work).
141
- - **Graph persistence**: mutate a whole object graph and flush once with `session.commit()`.
142
- - **Partial updates**: use `session.patchGraph()` to update only specific fields of an entity and its relations (returns `null` if entity doesn't exist).
143
- - **Relation change processor** that knows how to deal with has-many and many-to-many pivot tables.
144
- - **Interceptors**: `beforeFlush` / `afterFlush` hooks for cross-cutting concerns (auditing, multi-tenant filters, soft delete filters, etc.).
145
- - **Domain events**: `addDomainEvent` and a DomainEventBus integrated into `session.commit()`, aligned with domain events from [Domain-driven design](https://en.wikipedia.org/wiki/Domain-driven_design).
146
- - **JSON-safe entities**: relation wrappers hide internal references and implement `toJSON`, so `JSON.stringify` of hydrated entities works without circular reference errors.
147
-
148
- Use this layer where:
149
-
150
- - A request-scoped context fits (web/API handlers, jobs).
151
- - You want change tracking, cascades, and relation helpers instead of manual SQL for every update.
152
-
153
- ### Level 3 – Decorator entities
154
-
155
- If you like explicit model classes, you can add a thin decorator layer on top of the same schema/runtime:
156
-
157
- - `@Entity()` on a class to derive and register a table name (by default snake_case plural of the class name, with an optional `tableName` override).
158
- - `@Column(...)` and `@PrimaryKey(...)` on properties; decorators collect column metadata and later build `TableDef`s from it.
159
- - Relation decorators:
160
- - `@HasMany({ target, foreignKey, ... })`
161
- - `@HasOne({ target, foreignKey, ... })`
162
- - `@BelongsTo({ target, foreignKey, ... })`
163
- - `@BelongsToMany({ target, pivotTable, ... })`
164
- - `@MorphOne({ target, morphName, typeValue, ... })`
165
- - `@MorphMany({ target, morphName, typeValue, ... })`
166
- - `@MorphTo({ typeField, idField, targets, ... })`
167
- - `bootstrapEntities()` scans metadata, builds `TableDef`s, wires relations with the same `hasOne` / `hasMany` / `belongsTo` / `belongsToMany` helpers you would use manually, and returns the resulting tables. (If you forget to call it, `getTableDefFromEntity` / `selectFromEntity` will bootstrap lazily on first use, but bootstrapping once at startup lets you reuse the same table defs and generate schema SQL.)
168
- - `selectFromEntity(MyEntity)` lets you start a `SelectQueryBuilder` directly from the class. By default, `execute(session)` returns actual entity instances with all columns selected.
169
- - **Generate entities from an existing DB**: `npx metal-orm-gen -- --dialect=postgres --url=$DATABASE_URL --schema=public --out=src/entities.ts` introspects your schema and spits out `@Entity` / `@Column` classes you can immediately `bootstrapEntities()` with.
170
-
171
- You don’t have to use decorators, but when you do, you’re still on the same AST + dialect + runtime foundation.
172
-
173
- ---
174
-
175
- <a id="installation"></a>
176
- ## Installation πŸ“¦
177
-
178
- **Requirements:** Node.js β‰₯ 20.0.0. For TypeScript projects, use TS 5.6+ to get the standard decorators API and typings.
179
-
180
- ```bash
181
- # npm
182
- npm install metal-orm
183
-
184
- # yarn
185
- yarn add metal-orm
186
-
187
- # pnpm
188
- pnpm add metal-orm
189
- ```
190
-
191
- MetalORM compiles SQL; you bring your own driver:
192
-
193
- | Dialect | Driver | Install |
194
- | ------------------ | ----------------- | ------------------------------- |
195
- | MySQL / MariaDB | `mysql2` | `npm install mysql2` |
196
- | SQLite | `sqlite3` | `npm install sqlite3` |
197
- | SQLite | `better-sqlite3` | `npm install better-sqlite3` |
198
- | PostgreSQL | `pg` | `npm install pg` |
199
- | SQL Server | `tedious` | `npm install tedious` |
200
-
201
- Pick the matching dialect (`MySqlDialect`, `SQLiteDialect`, `PostgresDialect`, `MSSQLDialect`) when compiling queries.
202
-
203
- > Drivers are declared as optional peer dependencies. Install only the ones you actually use in your project.
204
-
205
- **Optional: Caching Backends**
206
-
207
- For production caching, choose based on your needs:
208
-
209
- | Adapter | Tags | Install | Use Case |
210
- |---------|------|---------|----------|
211
- | `RedisCacheAdapter` | βœ… Full support | `npm install ioredis` | Production with tag invalidation |
212
- | `KeyvCacheAdapter` | ❌ Not supported | `npm install keyv @keyv/redis` | Simple production setups |
213
-
214
- ```bash
215
- # For full-featured Redis (recommended)
216
- npm install ioredis
217
-
218
- # For simple Keyv-based caching
219
- npm install keyv @keyv/redis
220
- ```
221
-
222
- > Caching packages are optional peer dependencies. MetalORM includes `MemoryCacheAdapter` for development without external dependencies.
223
-
224
- ### Playground (optional) πŸ§ͺ
225
-
226
- The React playground lives in `playground/` and is no longer part of the published package or its dependency tree. To run it locally:
227
-
228
- 1. `cd playground && npm install`
229
- 2. `npm run dev` (uses the root `vite.config.ts`)
230
-
231
- It boots against an in-memory SQLite database seeded from fixtures under `playground/shared/`.
232
-
233
- ---
234
-
235
- <a id="quick-start"></a>
236
- ## Quick start – three levels
237
-
238
- <a id="level-1"></a>
239
- ### Level 1: Query builder & hydration 🧩
240
-
241
- #### 1. Tiny table, tiny query
242
-
243
- MetalORM can be just a straightforward query builder.
244
-
245
- ```ts
246
- import mysql from 'mysql2/promise';
247
- import {
248
- defineTable,
249
- tableRef,
250
- col,
251
- selectFrom,
252
- eq,
253
- MySqlDialect,
254
- } from 'metal-orm';
255
-
256
- // 1) A very small table
257
- const todos = defineTable('todos', {
258
- id: col.primaryKey(col.int()),
259
- title: col.varchar(255),
260
- done: col.boolean(),
261
- });
262
- // Add constraints
263
- todos.columns.title.notNull = true;
264
- todos.columns.done.default = false;
265
-
266
- // Optional: opt-in ergonomic column access
267
- const t = tableRef(todos);
268
-
269
- // 2) Build a simple query
270
- const listOpenTodos = selectFrom(todos)
271
- .select('id', 'title', 'done')
272
- .where(eq(t.done, false))
273
- .orderBy(t.id, 'ASC');
274
-
275
- // 3) Compile to SQL + params
276
- const dialect = new MySqlDialect();
277
- const { sql, params } = listOpenTodos.compile(dialect);
278
-
279
- // 4) Run with your favorite driver
280
- const connection = await mysql.createConnection({ /* ... */ });
281
- const [rows] = await connection.execute(sql, params);
282
-
283
- console.log(rows);
284
- // [
285
- // { id: 1, title: 'Write docs', done: 0 },
286
- // { id: 2, title: 'Ship feature', done: 0 },
287
- // ]
288
- ```
289
-
290
- If you keep a reusable array of column names (e.g. shared across helpers or pulled from config), you can spread it into `.select(...)` since the method accepts rest arguments:
291
-
292
- ```ts
293
- const defaultColumns = ['id', 'title', 'done'] as const;
294
- const listOpenTodos = selectFrom(todos).select(...defaultColumns);
295
- ```
296
-
297
- That's it: schema, query, SQL, done.
298
-
299
- If you are using the Level 2 runtime (`OrmSession`), `SelectQueryBuilder` also provides `count(session)`, `executePaged(session, { page, pageSize })`, and `executeCursor(session, { first/after | last/before })` for common pagination patterns. See [docs/pagination.md](./docs/pagination.md) for offset pagination, eager-include pagination guards, and bidirectional cursor pagination.
300
-
301
- #### Column pickers (preferred selection helpers)
302
-
303
- `defineTable` still exposes the full `table.columns` map for schema metadata and constraint tweaks, but modern queries usually benefit from higher-level helpers instead of spelling `todo.columns.*` everywhere.
304
-
305
- ```ts
306
- const t = tableRef(todos);
307
-
308
- const listOpenTodos = selectFrom(todos)
309
- .select('id', 'title', 'done') // typed shorthand for the same fields
310
- .where(eq(t.done, false))
311
- .orderBy(t.id, 'ASC');
312
- ```
313
-
314
- `select`, `include` (with `columns`), `includePick`, `selectColumnsDeep`, the `sel()` helpers for tables, and `esel()` for entities all build typed selection maps without repeating `table.columns.*`. Use those helpers when building query selections and reserve `table.columns.*` for schema definition, relations, or rare cases where you need a column reference outside of a picker. See the [Query Builder docs](./docs/query-builder.md#selection-helpers) for the reference, examples, and best practices for these helpers.
315
-
316
- #### Ergonomic column access (opt-in) with `tableRef`
317
-
318
- If you still want the convenience of accessing columns without spelling `.columns`, you can opt-in with `tableRef()`:
319
-
320
- ```ts
321
- import { tableRef, eq, selectFrom } from 'metal-orm';
322
-
323
- // Existing style (always works)
324
- const listOpenTodos = selectFrom(todos)
325
- .select('id', 'title', 'done')
326
- .where(eq(todos.columns.done, false))
327
- .orderBy(todos.columns.id, 'ASC');
328
-
329
- // Opt-in ergonomic style
330
- const t = tableRef(todos);
331
-
332
- const listOpenTodos2 = selectFrom(todos)
333
- .select('id', 'title', 'done')
334
- .where(eq(t.done, false))
335
- .orderBy(t.id, 'ASC');
336
- ```
337
-
338
- Collision rule: real table fields win.
339
-
340
- - `t.name` is the table name (string)
341
- - `t.$.name` is the column definition for a colliding column name (escape hatch)
342
-
343
- #### 2. Relations & hydration (still no ORM)
344
-
345
- Now add relations and get nested objects, still without committing to a runtime.
346
-
347
- ```ts
348
- import {
349
- defineTable,
350
- col,
351
- hasMany,
352
- selectFrom,
353
- eq,
354
- count,
355
- rowNumber,
356
- MySqlDialect,
357
- sel,
358
- hydrateRows,
359
- } from 'metal-orm';
360
-
361
- const posts = defineTable('posts', {
362
- id: col.primaryKey(col.int()),
363
- title: col.varchar(255),
364
- userId: col.int(),
365
- createdAt: col.timestamp(),
366
- });
367
-
368
- // Add constraints
369
- posts.columns.title.notNull = true;
370
- posts.columns.userId.notNull = true;
371
-
372
- const users = defineTable('users', {
373
- id: col.primaryKey(col.int()),
374
- name: col.varchar(255),
375
- email: col.varchar(255),
376
- });
377
-
378
- // Add relations and constraints
379
- users.relations = {
380
- posts: hasMany(posts, 'userId'),
381
- };
382
- users.columns.name.notNull = true;
383
- users.columns.email.unique = true;
384
-
385
- // Build a query with relation & window function
386
- const u = sel(users, 'id', 'name', 'email');
387
- const p = sel(posts, 'id', 'userId');
388
-
389
- const builder = selectFrom(users)
390
- .select({
391
- ...u,
392
- postCount: count(p.id),
393
- rank: rowNumber(), // window function helper
394
- })
395
- .leftJoin(posts, eq(p.userId, u.id))
396
- .groupBy(u.id)
397
- .groupBy(u.name)
398
- .groupBy(u.email)
399
- .orderBy(count(p.id), 'DESC')
400
- .limit(10)
401
- .includePick('posts', ['id', 'title', 'createdAt']); // eager relation for hydration
402
-
403
- const dialect = new MySqlDialect();
404
- const { sql, params } = builder.compile(dialect);
405
- const [rows] = await connection.execute(sql, params);
406
-
407
- // Turn flat rows into nested objects
408
- const hydrated = hydrateRows(
409
- rows as Record<string, unknown>[],
410
- builder.getHydrationPlan(),
411
- );
412
-
413
- console.log(hydrated);
414
- // [
415
- // {
416
- // id: 1,
417
- // name: 'John Doe',
418
- // email: 'john@example.com',
419
- // postCount: 15,
420
- // rank: 1,
421
- // posts: [
422
- // { id: 101, title: 'Latest Post', createdAt: '2023-05-15T10:00:00Z' },
423
- // // ...
424
- // ],
425
- // },
426
- // // ...
427
- // ]
428
- ```
429
-
430
- Use this mode anywhere you want powerful SQL + nice nested results, without changing how you manage your models.
431
-
432
- <a id="level-2"></a>
433
- ### Level 2: Entities + Unit of Work (ORM runtime) 🧠
434
-
435
- When you're ready, you can let MetalORM manage entities and relations for you.
436
-
437
- Instead of β€œnaked objects”, your queries can return entities attached to an `OrmSession`:
438
-
439
- ```ts
440
- import mysql from 'mysql2/promise';
441
- import {
442
- Orm,
443
- OrmSession,
444
- MySqlDialect,
445
- selectFrom,
446
- eq,
447
- tableRef,
448
- createMysqlExecutor,
449
- } from 'metal-orm';
450
-
451
- // 1) Create an Orm + session for this request
452
-
453
- const connection = await mysql.createConnection({ /* ... */ });
454
- const executor = createMysqlExecutor(connection);
455
- const orm = new Orm({
456
- dialect: new MySqlDialect(),
457
- executorFactory: {
458
- createExecutor: () => executor,
459
- createTransactionalExecutor: () => executor,
460
- dispose: async () => {},
461
- },
462
- });
463
- const session = new OrmSession({ orm, executor });
464
-
465
- const u = tableRef(users);
466
-
467
- // 2) Load entities with lazy relations
468
- const [user] = await selectFrom(users)
469
- .select('id', 'name', 'email')
470
- .includeLazy('posts') // HasMany as a lazy collection
471
- .includeLazy('roles') // BelongsToMany as a lazy collection
472
- .where(eq(u.id, 1))
473
- .execute(session);
474
-
475
- // user is an EntityInstance<typeof users>
476
- // scalar props are normal:
477
- user.name = 'Updated Name'; // marks entity as Dirty
478
-
479
- // relations are live collections:
480
- const postsCollection = await user.posts.load(); // batched lazy load
481
- const newPost = user.posts.add({ title: 'Hello from ORM mode' });
482
-
483
- // Many-to-many via pivot:
484
- await user.roles.syncByIds([1, 2, 3]);
485
-
486
- // 3) Persist the entire graph
487
- await session.commit();
488
- // INSERT/UPDATE/DELETE + pivot updates happen in a single Unit of Work.
489
- ```
490
-
491
- What the runtime gives you:
492
-
493
- - [Identity map](https://en.wikipedia.org/wiki/Identity_map_pattern) (per context).
494
- - [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) style change tracking on scalar properties.
495
- - Relation tracking (add/remove/sync on collections).
496
- - Cascades on relations: `'all' | 'persist' | 'remove' | 'link'`.
497
- - Single flush: `session.commit()` figures out inserts, updates, deletes, and pivot changes.
498
- - Column pickers to stay DRY: `select` on the root table, `include` (with `columns`) or `includePick` on relations, and `selectColumnsDeep` or the `sel`/`esel` helpers to build typed selection maps without repeating `table.columns.*`.
499
- - Tip: if you assign relations after `defineTable`, use `setRelations(table, { ... })` so TypeScript can validate `include(..., { columns: [...] })` and pivot columns. See `docs/query-builder.md`.
500
-
501
- <a id="level-3"></a>
502
- ### Level 3: Decorator entities ✨
503
-
504
- Finally, you can describe your models with decorators and still use the same runtime and query builder.
505
-
506
- The decorator layer is built on the TC39 Stage 3 standard (TypeScript 5.6+), so you simply decorate class fields (or accessors if you need custom logic) and the standard `ClassFieldDecoratorContext` keeps a metadata bag on `context.metadata`/`Symbol.metadata`. `@Entity` reads that bag when it runs and builds your `TableDef`sβ€”no `experimentalDecorators`, parameter decorators, or extra polyfills required.
507
-
508
- ```ts
509
- import mysql from 'mysql2/promise';
510
- import {
511
- Orm,
512
- OrmSession,
513
- MySqlDialect,
514
- col,
515
- createMysqlExecutor,
516
- Entity,
517
- Column,
518
- PrimaryKey,
519
- HasMany,
520
- BelongsTo,
521
- bootstrapEntities,
522
- selectFromEntity,
523
- entityRef,
524
- eq,
525
- } from 'metal-orm';
526
-
527
- @Entity()
528
- class User {
529
- @PrimaryKey(col.int())
530
- id!: number;
531
-
532
- @Column(col.varchar(255))
533
- name!: string;
534
-
535
- @Column(col.varchar(255))
536
- email?: string;
537
-
538
- @HasMany({
539
- target: () => Post,
540
- foreignKey: 'userId',
541
- })
542
- posts!: any; // relation wrapper; type omitted for brevity
543
- }
544
-
545
- @Entity()
546
- class Post {
547
- @PrimaryKey(col.int())
548
- id!: number;
549
-
550
- @Column(col.varchar(255))
551
- title!: string;
552
-
553
- @Column(col.int())
554
- userId!: number;
555
-
556
- @BelongsTo({
557
- target: () => User,
558
- foreignKey: 'userId',
559
- })
560
- user!: any;
561
- }
562
-
563
- // 1) Bootstrap metadata once at startup (recommended so you reuse the same TableDefs)
564
- const tables = bootstrapEntities(); // getTableDefFromEntity/selectFromEntity can bootstrap lazily if you forget
565
- // tables: TableDef[] – compatible with the rest of MetalORM
566
-
567
- // 2) Create an Orm + session
568
- const connection = await mysql.createConnection({ /* ... */ });
569
- const executor = createMysqlExecutor(connection);
570
- const orm = new Orm({
571
- dialect: new MySqlDialect(),
572
- executorFactory: {
573
- createExecutor: () => executor,
574
- createTransactionalExecutor: () => executor,
575
- dispose: async () => {},
576
- },
577
- });
578
- const session = new OrmSession({ orm, executor });
579
-
580
- // 3) Query starting from the entity class
581
- const U = entityRef(User);
582
- const [user] = await selectFromEntity(User)
583
- .select('id', 'name')
584
- .includeLazy('posts')
585
- .where(eq(U.id, 1))
586
- .execute(session); // user is an actual instance of the User class!
587
-
588
- // Use executePlain() if you want raw POJOs instead of class instances
589
- // Return type is inferred from selected columns: { id: number; name: string }[]
590
- const rawUsers = await selectFromEntity(User)
591
- .select('id', 'name')
592
- .executePlain(session);
593
-
594
- // Use firstOrFail() to get a single record or throw if not found
595
- const admin = await selectFromEntity(User)
596
- .where(eq(U.role, 'admin'))
597
- .firstOrFail(session); // throws Error('No results found') if no match
598
-
599
- // firstOrFailPlain() works the same but returns a POJO
600
- const adminPlain = await selectFromEntity(User)
601
- .where(eq(U.role, 'admin'))
602
- .firstOrFailPlain(session);
603
-
604
- user.posts.add({ title: 'From decorators' });
605
- await session.commit();
606
- ```
607
-
608
- Note: relation helpers like `add`/`attach` are only available on tracked entities returned by `execute(session)`. `executePlain()` returns POJOs without relation wrappers, with return types inferred from your `.select()` callsβ€”no manual casting needed. Make sure the primary key (e.g. `id`) is selected so relation adds can link correctly.
609
-
610
- Tip: to keep selections terse, use `select`, `include` (with `columns`), or the `sel`/`esel` helpers instead of spelling `table.columns.*` over and over. By default, `selectFromEntity` selects all columns if you don't specify any.
611
-
612
-
613
- This level is nice when:
614
-
615
- - You want classes as your domain model, but don't want a separate schema DSL.
616
- - You like decorators for explicit mapping but still want AST-first SQL and a disciplined runtime.
617
-
618
- ---
619
-
620
- <a id="when-to-use-which-level"></a>
621
- ## When to use which level? πŸ€”
622
-
623
- - **Query builder + hydration (Level 1)**
624
- Great for reporting/analytics, existing codebases with their own models, and services that need strong SQL but minimal runtime magic.
625
-
626
- - **ORM runtime (Level 2)**
627
- Great for request-scoped application logic and domain modeling where lazy relations, cascades, and graph persistence pay off.
628
-
629
- - **Decorator entities (Level 3)**
630
- Great when you want class-based entities and decorators, but still want to keep the underlying architecture explicit and layered.
631
-
632
- All three levels share the same schema, AST, and dialects, so you can mix them as needed and migrate gradually.
633
-
634
- ---
635
-
636
- <a id="design-notes"></a>
637
- ## Design & Architecture πŸ—οΈ
638
-
639
- MetalORM is built on solid software engineering principles and proven design patterns.
640
-
641
- ### Architecture Layers
642
-
643
- ```
644
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
645
- β”‚ Your Application β”‚
646
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
647
- β”‚
648
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
649
- β”‚ β”‚ β”‚
650
- β–Ό β–Ό β–Ό
651
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
652
- β”‚ Level 1 β”‚ β”‚ Level 2 β”‚ β”‚ Level 3 β”‚
653
- β”‚ Query │◄────── ORM │◄──────Decoratorsβ”‚
654
- β”‚ Builder β”‚ β”‚ Runtime β”‚ β”‚ β”‚
655
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
656
- β”‚ β”‚ β”‚
657
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
658
- β–Ό
659
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
660
- β”‚ SQL AST β”‚
661
- β”‚ (Typed Nodes) β”‚
662
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
663
- β–Ό
664
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
665
- β”‚ Strategy Pattern: Dialects β”‚
666
- β”‚ MySQL | PostgreSQL | SQLite | SQL Server β”‚
667
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
668
- β–Ό
669
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
670
- β”‚ Database β”‚
671
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
672
- ```
673
-
674
- ### Design Patterns
675
-
676
- - **Strategy Pattern**: Pluggable dialects (MySQL, PostgreSQL, SQLite, SQL Server) and function renderers allow the same query to target different databases
677
- - **Visitor Pattern**: AST traversal for SQL compilation and expression processing
678
- - **Builder Pattern**: Fluent query builders (Select, Insert, Update, Delete) for constructing queries step-by-step
679
- - **Factory Pattern**: Dialect factory and executor creation abstract instantiation logic
680
- - **Unit of Work**: Change tracking and batch persistence in `OrmSession` coordinate all modifications
681
- - **Identity Map**: One entity instance per row within a session prevents duplicate object issues
682
- - **Interceptor/Pipeline**: Query interceptors and flush lifecycle hooks enable cross-cutting concerns
683
- - **Adapter Pattern**: Connection pooling adapters allow different pool implementations
684
-
685
- ### Type Safety
686
-
687
- - **Zero `any` types**: The entire src codebase contains zero `any` typesβ€”every value is properly typed
688
- - **100% typed public API**: Every public method, parameter, and return value is fully typed
689
- - **Full type inference**: From schema definition through query building to result hydration
690
- - **Compile-time safety**: Catch SQL errors at TypeScript compile time, not runtime
691
- - **Generic-driven**: Leverages TypeScript generics extensively for type propagation
692
-
693
- ### Separation of Concerns
694
-
695
- Each layer has a clear, focused responsibility:
696
-
697
- - **Core AST layer**: SQL representation independent of any specific dialect
698
- - **Dialect layer**: Vendor-specific SQL compilation (MySQL, PostgreSQL, etc.)
699
- - **Schema layer**: Table and column definitions with relations
700
- - **Query builder layer**: Fluent API for building type-safe queries
701
- - **Hydration layer**: Transforms flat result sets into nested object graphs
702
- - **ORM runtime layer**: Entity management, change tracking, lazy relations, transactions
703
-
704
- You can use just the layers you need and stay at the low level (AST + dialects) or adopt higher levels when beneficial.
705
-
706
- ---
707
-
708
- ## Frequently Asked Questions ❓
709
-
710
- **Q: How does MetalORM differ from other ORMs?**
711
- A: MetalORM's unique three-level architecture lets you choose your abstraction levelβ€”use just the query builder, add the ORM runtime when needed, or go full decorator-based entities. This gradual adoption path is uncommon in the TypeScript ecosystem. You're not locked into an all-or-nothing ORM approach.
712
-
713
- **Q: Can I use this in production?**
714
- A: Yes! MetalORM is designed for production use with robust patterns like Unit of Work, Identity Map, and connection pooling support. The type-safe query builder ensures SQL correctness at compile time.
715
-
716
- **Q: Do I need to use all three levels?**
717
- A: No! Use only what you need. Many projects stay at Level 1 (query builder) for its type-safe SQL building without any ORM overhead. Add runtime features (Level 2) or decorators (Level 3) only where they provide value.
718
-
719
- **Q: What about migrations?**
720
- A: MetalORM provides schema generation via DDL builders. See the [Schema Generation docs](./docs/schema-generation.md) for details on generating CREATE TABLE statements from your table definitions.
721
-
722
- **Q: How type-safe is it really?**
723
- A: Exceptionally. The entire codebase contains **zero** `any` typesβ€”every value is properly typed with TypeScript generics and inference. All public APIs are fully typed, and your queries, entities, and results get full TypeScript checking at compile time.
724
-
725
- **Q: What design patterns are used?**
726
- A: MetalORM implements several well-known patterns: Strategy (dialects & functions), Visitor (AST traversal), Builder (query construction), Factory (dialect & executor creation), Unit of Work (change tracking), Identity Map (entity caching), Interceptor (query hooks), and Adapter (pooling). This makes the codebase maintainable and extensible.
727
-
728
- ---
729
-
730
- ## Performance & Production πŸš€
731
-
732
- - **Zero runtime overhead for Level 1** (query builder) - it's just SQL compilation and hydration
733
- - **Efficient batching** for Level 2 lazy relations minimizes database round-trips
734
- - **Identity Map** prevents duplicate entity instances and unnecessary queries
735
- - **Connection pooling** supported via executor factory pattern (see [pooling docs](./docs/pooling.md))
736
- - **Prepared statements** with parameterized queries protect against SQL injection
737
-
738
- **Production checklist:**
739
- - βœ… Use connection pooling for better resource management
740
- - βœ… Enable query logging in development for debugging
741
- - βœ… Set up proper error handling and retries
742
- - βœ… Use transactions for multi-statement operations
743
- - βœ… Monitor query performance with interceptors
744
-
745
- ---
746
-
747
- ## Community & Support πŸ’¬
748
-
749
- - πŸ› **Issues:** [GitHub Issues](https://github.com/celsowm/metal-orm/issues)
750
- - πŸ’‘ **Discussions:** [GitHub Discussions](https://github.com/celsowm/metal-orm/discussions)
751
- - πŸ“– **Documentation:** [Full docs](./docs/index.md)
752
- - πŸ—ΊοΈ **Roadmap:** [See what's planned](./ROADMAP.md)
753
- - πŸ“¦ **Changelog:** [View releases](https://github.com/celsowm/metal-orm/releases)
754
-
755
- ---
756
-
757
- <a id="contributing"></a>
758
- ## Contributing 🀝
759
-
760
- Issues and PRs are welcome! If you're interested in pushing the runtime/ORM side further (soft deletes, multi-tenant filters, outbox patterns, etc.), contributions are especially appreciated.
761
-
762
- See the contributing guide for details.
763
-
764
- ---
765
-
766
- <a id="license"></a>
767
- ## License πŸ“„
768
-
769
- MetalORM is MIT licensed.
1
+ # MetalORM βš™οΈ
2
+
3
+ [![npm version](https://img.shields.io/npm/v/metal-orm.svg)](https://www.npmjs.com/package/metal-orm)
4
+ [![license](https://img.shields.io/npm/l/metal-orm.svg)](https://github.com/celsowm/metal-orm/blob/main/LICENSE)
5
+ [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%23007ACC.svg)](https://www.typescriptlang.org/)
6
+
7
+ > **TypeScript-first ORM that adapts to your needs**: use it as a type-safe query builder, a full-featured ORM runtime, or anything in between.
8
+
9
+ ## Why MetalORM? πŸ’‘
10
+
11
+ - 🎯 **Gradual adoption**: Start with just SQL building, add ORM features when you need them
12
+ - πŸ”’ **Exceptionally strongly typed**: Built with TypeScript generics and type inferenceβ€”**zero** `any` types in the entire codebase
13
+ - πŸ—οΈ **Well-architected**: Implements proven design patterns (Strategy, Visitor, Builder, Unit of Work, Identity Map, Interceptor, and more)
14
+ - 🎨 **One AST, multiple levels**: All features share the same SQL AST foundationβ€”no magic, just composable layers
15
+ - πŸš€ **Multi-dialect from the start**: MySQL, PostgreSQL, SQLite, SQL Server support built-in
16
+
17
+ ---
18
+
19
+ ## ⚑ 30-Second Quick Start
20
+
21
+ ```ts
22
+ import { defineTable, col, selectFrom, MySqlDialect } from 'metal-orm';
23
+
24
+ const users = defineTable('users', {
25
+ id: col.primaryKey(col.int()),
26
+ name: col.varchar(255),
27
+ });
28
+
29
+ const query = selectFrom(users).select('id', 'name').limit(10);
30
+ const { sql, params } = query.compile(new MySqlDialect());
31
+ // That's it! Use sql + params with any driver.
32
+ // ↑ Fully typedβ€”no casting, no 'any', just strong types all the way down
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Three Levels of Abstraction
38
+
39
+ MetalORM is a TypeScript-first, AST-driven SQL toolkit you can dial up or down depending on how "ORM-y" you want to be:
40
+
41
+ - **Level 1 – Query builder & hydration 🧩**
42
+ Define tables with `defineTable` / `col.*`, build strongly-typed queries on a real SQL AST, and hydrate flat result sets into nested objects – no ORM runtime involved.
43
+
44
+ - **Level 2 – ORM runtime (entities + Unit of Work 🧠)**
45
+ Let `OrmSession` (created from `Orm`) turn rows into tracked entities with lazy relations, cascades, and a [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) that flushes changes with `session.commit()`.
46
+
47
+ - **Level 3 – Decorator entities (classes + metadata ✨)**
48
+ Use `@Entity`, `@Column`, `@PrimaryKey`, relation decorators, `bootstrapEntities()` (or the lazy bootstrapping in `getTableDefFromEntity` / `selectFromEntity`) to describe your model classes. MetalORM bootstraps schema & relations from metadata and plugs them into the same runtime and query builder.
49
+
50
+ **Use only the layer you need in each part of your codebase.**
51
+
52
+ ---
53
+
54
+ <a id="table-of-contents"></a>
55
+ ## Table of Contents 🧭
56
+
57
+ - [Documentation](#documentation)
58
+ - [Features](#features)
59
+ - [Installation](#installation)
60
+ - [Quick start - three levels](#quick-start)
61
+ - [Level 1 – Query builder & hydration](#level-1)
62
+ - [Level 2 – Entities + Unit of Work](#level-2)
63
+ - [Level 3 – Decorator entities](#level-3)
64
+ - [When to use which level?](#when-to-use-which-level)
65
+ - [Design & Architecture](#design-notes)
66
+ - [FAQ](#frequently-asked-questions-)
67
+ - [Performance & Production](#performance--production-)
68
+ - [Community & Support](#community--support-)
69
+ - [Contributing](#contributing)
70
+ - [License](#license)
71
+
72
+ ---
73
+
74
+ <a id="documentation"></a>
75
+ ## Documentation πŸ“š
76
+
77
+ Full docs live in the `docs/` folder:
78
+
79
+ - [Introduction](https://github.com/celsowm/metal-orm/blob/main/docs/index.md)
80
+ - [Getting Started](https://github.com/celsowm/metal-orm/blob/main/docs/getting-started.md)
81
+ - [Level 3 Backend Tutorial](https://github.com/celsowm/metal-orm/blob/main/docs/level-3-backend-tutorial.md)
82
+ - [Schema Definition](https://github.com/celsowm/metal-orm/blob/main/docs/schema-definition.md)
83
+ - [Query Builder](https://github.com/celsowm/metal-orm/blob/main/docs/query-builder.md)
84
+ - [Tree Behavior (Nested Set/MPTT)](https://github.com/celsowm/metal-orm/blob/main/docs/tree.md)
85
+ - [DTO (Data Transfer Objects)](https://github.com/celsowm/metal-orm/blob/main/docs/dto.md)
86
+ - [OpenAPI Schema Generation](https://github.com/celsowm/metal-orm/blob/main/docs/openapi.md)
87
+ - [DML Operations](https://github.com/celsowm/metal-orm/blob/main/docs/dml-operations.md)
88
+ - [Hydration & Entities](https://github.com/celsowm/metal-orm/blob/main/docs/hydration.md)
89
+ - [Runtime & Unit of Work](https://github.com/celsowm/metal-orm/blob/main/docs/runtime.md)
90
+ - [Save Graph](https://github.com/celsowm/metal-orm/blob/main/docs/save-graph.md)
91
+ - [Caching](https://github.com/celsowm/metal-orm/blob/main/docs/caching.md)
92
+ - [Advanced Features](https://github.com/celsowm/metal-orm/blob/main/docs/advanced-features.md)
93
+ - [Multi-Dialect Support](https://github.com/celsowm/metal-orm/blob/main/docs/multi-dialect-support.md)
94
+ - [Schema Generation (DDL)](https://github.com/celsowm/metal-orm/blob/main/docs/schema-generation.md)
95
+ - [Polymorphic Relations](https://github.com/celsowm/metal-orm/blob/main/docs/relations/14-polymorphic-relations.md)
96
+ - [API Reference](https://github.com/celsowm/metal-orm/blob/main/docs/api-reference.md)
97
+ - [DB ➜ TS Type Mapping](https://github.com/celsowm/metal-orm/blob/main/docs/db-to-ts-types.md)
98
+ - [Stored Procedures](https://github.com/celsowm/metal-orm/blob/main/docs/stored-procedures.md)
99
+
100
+ ---
101
+
102
+ <a id="features"></a>
103
+ ## Features πŸš€
104
+
105
+ ### Level 1 – Query builder & hydration
106
+
107
+ - **Declarative schema definition** with `defineTable`, `col.*`, and typed relations (including polymorphic `morphOne`, `morphMany`, `morphTo`).
108
+ - **Typed temporal columns**: `col.date()` / `col.datetime()` / `col.timestamp()` default to `string` but accept a generic when your driver returns `Date` (e.g. `col.date<Date>()`).
109
+ - **Fluent query builder** over a real SQL AST
110
+ (`SelectQueryBuilder`, `InsertQueryBuilder`, `UpdateQueryBuilder`, `DeleteQueryBuilder`).
111
+ - **Advanced SQL**: CTEs, aggregates, window functions, subqueries, bitwise operators (`&`, `|`, `^`, `<<`, `>>`), JSON, CASE, EXISTS, and the full SQL function catalog (e.g. `STDDEV`, `VARIANCE`, `LOG2`, `CBRT`, `COALESCE`, `NULLIF`, `GREATEST`, `LEAST`, `IFNULL`, `LOCALTIME`, `LOCALTIMESTAMP`, `AGE`).
112
+ - **Table-valued functions**: use the new `tvf(key, …)` helper when you want portable intents such as `ARRAY_UNNEST`, letting the dialects’ `TableFunctionStrategy` renderers emit dialect-specific syntax (`LATERAL`/`WITH ORDINALITY`, alias validation, quoting, etc.). `fnTable()` remains available as the raw escape hatch when you need to emit a specific SQL function directly.
113
+ - **String helpers**: `lower`, `upper`, `trim`, `ltrim/rtrim`, `concat/concatWs`, `substr/left/right`, `position/instr/locate`, `replace`, `repeat`, `lpad/rpad`, `space`, and more with dialect-aware rendering.
114
+ - **Set operations**: `union`, `unionAll`, `intersect`, `except` across all dialects (ORDER/LIMIT apply to the combined result; hydration is disabled for compound queries so rows are returned as-is without collapsing duplicates).
115
+ - **Expression builders**: `eq`, `and`, `or`, `not`, `between`, `inList`, `exists`, `jsonPath`, `caseWhen`, window functions like `rowNumber`, `rank`, `lag`, `lead`, etc., all backed by typed AST nodes.
116
+ - **Operator safety**: scalar operators (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`) are for single values; for arrays, use `inList`/`notInList`.
117
+ - Migration example: `where(eq(tipoAcao.columns.codigo, codigos))` -> `where(inList(tipoAcao.columns.codigo, codigos))`.
118
+ - **Relation-aware hydration**: turn flat rows into nested objects (`user.posts`, `user.roles`, etc.) using a hydration plan derived from the AST metadata.
119
+ - **Multi-dialect**: compile once, run on MySQL/MariaDB, PostgreSQL, SQLite, or SQL Server via pluggable dialects.
120
+ - **DML**: type-safe INSERT / UPDATE / DELETE with `RETURNING` where supported.
121
+ - Includes upsert support via `.onConflict(...).doUpdate(...)` / `.doNothing()` with dialect-specific SQL generation.
122
+
123
+ Level 1 is ideal when you:
124
+
125
+ - Already have a domain model and just want a serious SQL builder.
126
+ - Want deterministic SQL (no magical query generation).
127
+ - Need to share the same AST across tooling (e.g. codegen, diagnostics, logging).
128
+
129
+ ### Level 2 – ORM runtime (`OrmSession`)
130
+
131
+ On top of the query builder, MetalORM ships a focused runtime managed by `Orm` and its request-scoped `OrmSession`s:
132
+
133
+ - **Entities inferred from your `TableDef`s** (no separate mapping file).
134
+ - **Lazy, batched relations**: `user.posts.load()`, `user.roles.syncByIds([...])`, etc.
135
+ - **Scoped transactions**: `session.transaction(async s => { ... })` wraps `begin/commit/rollback` and uses savepoints for nested calls on the same session; `Orm.transaction` remains available when you want a fresh transactional executor per call.
136
+ - **Identity map**: the same row becomes the same entity instance within a session (see the [Identity map pattern](https://en.wikipedia.org/wiki/Identity_map_pattern)).
137
+ - **Caching**: Flexible caching with `MemoryCacheAdapter` (dev), `KeyvCacheAdapter` (simple production), or `RedisCacheAdapter` (full-featured with tag support). Features human-readable TTL (`'30m'`, `'2h'`), tag-based invalidation, and multi-tenant cache isolation.
138
+ - **Tree Behavior (Nested Set/MPTT)**: hierarchical data with `TreeManager`, `treeQuery()`, and `@Tree` decorators. Efficient O(log n) operations for moves, inserts, and deletes. Supports multi-tree scoping, recovery, and validation.
139
+ - **DTO/OpenAPI helpers**: the `metal-orm/dto` module generates DTOs and OpenAPI schemas, including tree schemas (`TreeNode`, `TreeNodeResult`, threaded trees).
140
+ - **Unit of Work (`OrmSession`)** tracking New/Dirty/Removed entities and relation changes, inspired by the classic [Unit of Work pattern](https://en.wikipedia.org/wiki/wiki/Unit_of_work).
141
+ - **Graph persistence**: mutate a whole object graph and flush once with `session.commit()`.
142
+ - **Partial updates**: use `session.patchGraph()` to update only specific fields of an entity and its relations (returns `null` if entity doesn't exist).
143
+ - **Relation change processor** that knows how to deal with has-many and many-to-many pivot tables.
144
+ - **Interceptors**: `beforeFlush` / `afterFlush` hooks for cross-cutting concerns (auditing, multi-tenant filters, soft delete filters, etc.).
145
+ - **Domain events**: `addDomainEvent` and a DomainEventBus integrated into `session.commit()`, aligned with domain events from [Domain-driven design](https://en.wikipedia.org/wiki/Domain-driven_design).
146
+ - **JSON-safe entities**: relation wrappers hide internal references and implement `toJSON`, so `JSON.stringify` of hydrated entities works without circular reference errors.
147
+
148
+ Use this layer where:
149
+
150
+ - A request-scoped context fits (web/API handlers, jobs).
151
+ - You want change tracking, cascades, and relation helpers instead of manual SQL for every update.
152
+
153
+ ### Level 3 – Decorator entities
154
+
155
+ If you like explicit model classes, you can add a thin decorator layer on top of the same schema/runtime:
156
+
157
+ - `@Entity()` on a class to derive and register a table name (by default snake_case plural of the class name, with an optional `tableName` override).
158
+ - `@Column(...)` and `@PrimaryKey(...)` on properties; decorators collect column metadata and later build `TableDef`s from it.
159
+ - Relation decorators:
160
+ - `@HasMany({ target, foreignKey, ... })`
161
+ - `@HasOne({ target, foreignKey, ... })`
162
+ - `@BelongsTo({ target, foreignKey, ... })`
163
+ - `@BelongsToMany({ target, pivotTable, ... })`
164
+ - `@MorphOne({ target, morphName, typeValue, ... })`
165
+ - `@MorphMany({ target, morphName, typeValue, ... })`
166
+ - `@MorphTo({ typeField, idField, targets, ... })`
167
+ - `bootstrapEntities()` scans metadata, builds `TableDef`s, wires relations with the same `hasOne` / `hasMany` / `belongsTo` / `belongsToMany` helpers you would use manually, and returns the resulting tables. (If you forget to call it, `getTableDefFromEntity` / `selectFromEntity` will bootstrap lazily on first use, but bootstrapping once at startup lets you reuse the same table defs and generate schema SQL.)
168
+ - `selectFromEntity(MyEntity)` lets you start a `SelectQueryBuilder` directly from the class. By default, `execute(session)` returns actual entity instances with all columns selected.
169
+ - **Generate entities from an existing DB**: `npx metal-orm-gen -- --dialect=postgres --url=$DATABASE_URL --schema=public --out=src/entities.ts` introspects your schema and spits out `@Entity` / `@Column` classes you can immediately `bootstrapEntities()` with.
170
+
171
+ You don’t have to use decorators, but when you do, you’re still on the same AST + dialect + runtime foundation.
172
+
173
+ ---
174
+
175
+ <a id="installation"></a>
176
+ ## Installation πŸ“¦
177
+
178
+ **Requirements:** Node.js β‰₯ 20.0.0. For TypeScript projects, use TS 5.6+ to get the standard decorators API and typings.
179
+
180
+ ```bash
181
+ # npm
182
+ npm install metal-orm
183
+
184
+ # yarn
185
+ yarn add metal-orm
186
+
187
+ # pnpm
188
+ pnpm add metal-orm
189
+ ```
190
+
191
+ MetalORM compiles SQL; you bring your own driver:
192
+
193
+ | Dialect | Driver | Install |
194
+ | ------------------ | ----------------- | ------------------------------- |
195
+ | MySQL / MariaDB | `mysql2` | `npm install mysql2` |
196
+ | SQLite | `sqlite3` | `npm install sqlite3` |
197
+ | SQLite | `better-sqlite3` | `npm install better-sqlite3` |
198
+ | PostgreSQL | `pg` | `npm install pg` |
199
+ | SQL Server | `tedious` | `npm install tedious` |
200
+
201
+ Pick the matching dialect (`MySqlDialect`, `SQLiteDialect`, `PostgresDialect`, `MSSQLDialect`) when compiling queries.
202
+
203
+ > Drivers are declared as optional peer dependencies. Install only the ones you actually use in your project.
204
+
205
+ **Optional: Caching Backends**
206
+
207
+ For production caching, choose based on your needs:
208
+
209
+ | Adapter | Tags | Install | Use Case |
210
+ |---------|------|---------|----------|
211
+ | `RedisCacheAdapter` | βœ… Full support | `npm install ioredis` | Production with tag invalidation |
212
+ | `KeyvCacheAdapter` | ❌ Not supported | `npm install keyv @keyv/redis` | Simple production setups |
213
+
214
+ ```bash
215
+ # For full-featured Redis (recommended)
216
+ npm install ioredis
217
+
218
+ # For simple Keyv-based caching
219
+ npm install keyv @keyv/redis
220
+ ```
221
+
222
+ > Caching packages are optional peer dependencies. MetalORM includes `MemoryCacheAdapter` for development without external dependencies.
223
+
224
+ <a id="quick-start"></a>
225
+ ## Quick start – three levels
226
+
227
+ <a id="level-1"></a>
228
+ ### Level 1: Query builder & hydration 🧩
229
+
230
+ #### 1. Tiny table, tiny query
231
+
232
+ MetalORM can be just a straightforward query builder.
233
+
234
+ ```ts
235
+ import mysql from 'mysql2/promise';
236
+ import {
237
+ defineTable,
238
+ tableRef,
239
+ col,
240
+ selectFrom,
241
+ eq,
242
+ MySqlDialect,
243
+ } from 'metal-orm';
244
+
245
+ // 1) A very small table
246
+ const todos = defineTable('todos', {
247
+ id: col.primaryKey(col.int()),
248
+ title: col.varchar(255),
249
+ done: col.boolean(),
250
+ });
251
+ // Add constraints
252
+ todos.columns.title.notNull = true;
253
+ todos.columns.done.default = false;
254
+
255
+ // Optional: opt-in ergonomic column access
256
+ const t = tableRef(todos);
257
+
258
+ // 2) Build a simple query
259
+ const listOpenTodos = selectFrom(todos)
260
+ .select('id', 'title', 'done')
261
+ .where(eq(t.done, false))
262
+ .orderBy(t.id, 'ASC');
263
+
264
+ // 3) Compile to SQL + params
265
+ const dialect = new MySqlDialect();
266
+ const { sql, params } = listOpenTodos.compile(dialect);
267
+
268
+ // 4) Run with your favorite driver
269
+ const connection = await mysql.createConnection({ /* ... */ });
270
+ const [rows] = await connection.execute(sql, params);
271
+
272
+ console.log(rows);
273
+ // [
274
+ // { id: 1, title: 'Write docs', done: 0 },
275
+ // { id: 2, title: 'Ship feature', done: 0 },
276
+ // ]
277
+ ```
278
+
279
+ If you keep a reusable array of column names (e.g. shared across helpers or pulled from config), you can spread it into `.select(...)` since the method accepts rest arguments:
280
+
281
+ ```ts
282
+ const defaultColumns = ['id', 'title', 'done'] as const;
283
+ const listOpenTodos = selectFrom(todos).select(...defaultColumns);
284
+ ```
285
+
286
+ That's it: schema, query, SQL, done.
287
+
288
+ If you are using the Level 2 runtime (`OrmSession`), `SelectQueryBuilder` also provides `count(session)`, `executePaged(session, { page, pageSize })`, and `executeCursor(session, { first/after | last/before })` for common pagination patterns. See [docs/pagination.md](./docs/pagination.md) for offset pagination, eager-include pagination guards, and bidirectional cursor pagination.
289
+
290
+ #### Column pickers (preferred selection helpers)
291
+
292
+ `defineTable` still exposes the full `table.columns` map for schema metadata and constraint tweaks, but modern queries usually benefit from higher-level helpers instead of spelling `todo.columns.*` everywhere.
293
+
294
+ ```ts
295
+ const t = tableRef(todos);
296
+
297
+ const listOpenTodos = selectFrom(todos)
298
+ .select('id', 'title', 'done') // typed shorthand for the same fields
299
+ .where(eq(t.done, false))
300
+ .orderBy(t.id, 'ASC');
301
+ ```
302
+
303
+ `select`, `include` (with `columns`), `includePick`, `selectColumnsDeep`, the `sel()` helpers for tables, and `esel()` for entities all build typed selection maps without repeating `table.columns.*`. Use those helpers when building query selections and reserve `table.columns.*` for schema definition, relations, or rare cases where you need a column reference outside of a picker. See the [Query Builder docs](./docs/query-builder.md#selection-helpers) for the reference, examples, and best practices for these helpers.
304
+
305
+ #### Ergonomic column access (opt-in) with `tableRef`
306
+
307
+ If you still want the convenience of accessing columns without spelling `.columns`, you can opt-in with `tableRef()`:
308
+
309
+ ```ts
310
+ import { tableRef, eq, selectFrom } from 'metal-orm';
311
+
312
+ // Existing style (always works)
313
+ const listOpenTodos = selectFrom(todos)
314
+ .select('id', 'title', 'done')
315
+ .where(eq(todos.columns.done, false))
316
+ .orderBy(todos.columns.id, 'ASC');
317
+
318
+ // Opt-in ergonomic style
319
+ const t = tableRef(todos);
320
+
321
+ const listOpenTodos2 = selectFrom(todos)
322
+ .select('id', 'title', 'done')
323
+ .where(eq(t.done, false))
324
+ .orderBy(t.id, 'ASC');
325
+ ```
326
+
327
+ Collision rule: real table fields win.
328
+
329
+ - `t.name` is the table name (string)
330
+ - `t.$.name` is the column definition for a colliding column name (escape hatch)
331
+
332
+ #### 2. Relations & hydration (still no ORM)
333
+
334
+ Now add relations and get nested objects, still without committing to a runtime.
335
+
336
+ ```ts
337
+ import {
338
+ defineTable,
339
+ col,
340
+ hasMany,
341
+ selectFrom,
342
+ eq,
343
+ count,
344
+ rowNumber,
345
+ MySqlDialect,
346
+ sel,
347
+ hydrateRows,
348
+ } from 'metal-orm';
349
+
350
+ const posts = defineTable('posts', {
351
+ id: col.primaryKey(col.int()),
352
+ title: col.varchar(255),
353
+ userId: col.int(),
354
+ createdAt: col.timestamp(),
355
+ });
356
+
357
+ // Add constraints
358
+ posts.columns.title.notNull = true;
359
+ posts.columns.userId.notNull = true;
360
+
361
+ const users = defineTable('users', {
362
+ id: col.primaryKey(col.int()),
363
+ name: col.varchar(255),
364
+ email: col.varchar(255),
365
+ });
366
+
367
+ // Add relations and constraints
368
+ users.relations = {
369
+ posts: hasMany(posts, 'userId'),
370
+ };
371
+ users.columns.name.notNull = true;
372
+ users.columns.email.unique = true;
373
+
374
+ // Build a query with relation & window function
375
+ const u = sel(users, 'id', 'name', 'email');
376
+ const p = sel(posts, 'id', 'userId');
377
+
378
+ const builder = selectFrom(users)
379
+ .select({
380
+ ...u,
381
+ postCount: count(p.id),
382
+ rank: rowNumber(), // window function helper
383
+ })
384
+ .leftJoin(posts, eq(p.userId, u.id))
385
+ .groupBy(u.id)
386
+ .groupBy(u.name)
387
+ .groupBy(u.email)
388
+ .orderBy(count(p.id), 'DESC')
389
+ .limit(10)
390
+ .includePick('posts', ['id', 'title', 'createdAt']); // eager relation for hydration
391
+
392
+ const dialect = new MySqlDialect();
393
+ const { sql, params } = builder.compile(dialect);
394
+ const [rows] = await connection.execute(sql, params);
395
+
396
+ // Turn flat rows into nested objects
397
+ const hydrated = hydrateRows(
398
+ rows as Record<string, unknown>[],
399
+ builder.getHydrationPlan(),
400
+ );
401
+
402
+ console.log(hydrated);
403
+ // [
404
+ // {
405
+ // id: 1,
406
+ // name: 'John Doe',
407
+ // email: 'john@example.com',
408
+ // postCount: 15,
409
+ // rank: 1,
410
+ // posts: [
411
+ // { id: 101, title: 'Latest Post', createdAt: '2023-05-15T10:00:00Z' },
412
+ // // ...
413
+ // ],
414
+ // },
415
+ // // ...
416
+ // ]
417
+ ```
418
+
419
+ Use this mode anywhere you want powerful SQL + nice nested results, without changing how you manage your models.
420
+
421
+ <a id="level-2"></a>
422
+ ### Level 2: Entities + Unit of Work (ORM runtime) 🧠
423
+
424
+ When you're ready, you can let MetalORM manage entities and relations for you.
425
+
426
+ Instead of β€œnaked objects”, your queries can return entities attached to an `OrmSession`:
427
+
428
+ ```ts
429
+ import mysql from 'mysql2/promise';
430
+ import {
431
+ Orm,
432
+ OrmSession,
433
+ MySqlDialect,
434
+ selectFrom,
435
+ eq,
436
+ tableRef,
437
+ createMysqlExecutor,
438
+ } from 'metal-orm';
439
+
440
+ // 1) Create an Orm + session for this request
441
+
442
+ const connection = await mysql.createConnection({ /* ... */ });
443
+ const executor = createMysqlExecutor(connection);
444
+ const orm = new Orm({
445
+ dialect: new MySqlDialect(),
446
+ executorFactory: {
447
+ createExecutor: () => executor,
448
+ createTransactionalExecutor: () => executor,
449
+ dispose: async () => {},
450
+ },
451
+ });
452
+ const session = new OrmSession({ orm, executor });
453
+
454
+ const u = tableRef(users);
455
+
456
+ // 2) Load entities with lazy relations
457
+ const [user] = await selectFrom(users)
458
+ .select('id', 'name', 'email')
459
+ .includeLazy('posts') // HasMany as a lazy collection
460
+ .includeLazy('roles') // BelongsToMany as a lazy collection
461
+ .where(eq(u.id, 1))
462
+ .execute(session);
463
+
464
+ // user is an EntityInstance<typeof users>
465
+ // scalar props are normal:
466
+ user.name = 'Updated Name'; // marks entity as Dirty
467
+
468
+ // relations are live collections:
469
+ const postsCollection = await user.posts.load(); // batched lazy load
470
+ const newPost = user.posts.add({ title: 'Hello from ORM mode' });
471
+
472
+ // Many-to-many via pivot:
473
+ await user.roles.syncByIds([1, 2, 3]);
474
+
475
+ // 3) Persist the entire graph
476
+ await session.commit();
477
+ // INSERT/UPDATE/DELETE + pivot updates happen in a single Unit of Work.
478
+ ```
479
+
480
+ What the runtime gives you:
481
+
482
+ - [Identity map](https://en.wikipedia.org/wiki/Identity_map_pattern) (per context).
483
+ - [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) style change tracking on scalar properties.
484
+ - Relation tracking (add/remove/sync on collections).
485
+ - Cascades on relations: `'all' | 'persist' | 'remove' | 'link'`.
486
+ - Single flush: `session.commit()` figures out inserts, updates, deletes, and pivot changes.
487
+ - Column pickers to stay DRY: `select` on the root table, `include` (with `columns`) or `includePick` on relations, and `selectColumnsDeep` or the `sel`/`esel` helpers to build typed selection maps without repeating `table.columns.*`.
488
+ - Tip: if you assign relations after `defineTable`, use `setRelations(table, { ... })` so TypeScript can validate `include(..., { columns: [...] })` and pivot columns. See `docs/query-builder.md`.
489
+
490
+ <a id="level-3"></a>
491
+ ### Level 3: Decorator entities ✨
492
+
493
+ Finally, you can describe your models with decorators and still use the same runtime and query builder.
494
+
495
+ The decorator layer is built on the TC39 Stage 3 standard (TypeScript 5.6+), so you simply decorate class fields (or accessors if you need custom logic) and the standard `ClassFieldDecoratorContext` keeps a metadata bag on `context.metadata`/`Symbol.metadata`. `@Entity` reads that bag when it runs and builds your `TableDef`sβ€”no `experimentalDecorators`, parameter decorators, or extra polyfills required.
496
+
497
+ ```ts
498
+ import mysql from 'mysql2/promise';
499
+ import {
500
+ Orm,
501
+ OrmSession,
502
+ MySqlDialect,
503
+ col,
504
+ createMysqlExecutor,
505
+ Entity,
506
+ Column,
507
+ PrimaryKey,
508
+ HasMany,
509
+ BelongsTo,
510
+ bootstrapEntities,
511
+ selectFromEntity,
512
+ entityRef,
513
+ eq,
514
+ } from 'metal-orm';
515
+
516
+ @Entity()
517
+ class User {
518
+ @PrimaryKey(col.int())
519
+ id!: number;
520
+
521
+ @Column(col.varchar(255))
522
+ name!: string;
523
+
524
+ @Column(col.varchar(255))
525
+ email?: string;
526
+
527
+ @HasMany({
528
+ target: () => Post,
529
+ foreignKey: 'userId',
530
+ })
531
+ posts!: any; // relation wrapper; type omitted for brevity
532
+ }
533
+
534
+ @Entity()
535
+ class Post {
536
+ @PrimaryKey(col.int())
537
+ id!: number;
538
+
539
+ @Column(col.varchar(255))
540
+ title!: string;
541
+
542
+ @Column(col.int())
543
+ userId!: number;
544
+
545
+ @BelongsTo({
546
+ target: () => User,
547
+ foreignKey: 'userId',
548
+ })
549
+ user!: any;
550
+ }
551
+
552
+ // 1) Bootstrap metadata once at startup (recommended so you reuse the same TableDefs)
553
+ const tables = bootstrapEntities(); // getTableDefFromEntity/selectFromEntity can bootstrap lazily if you forget
554
+ // tables: TableDef[] – compatible with the rest of MetalORM
555
+
556
+ // 2) Create an Orm + session
557
+ const connection = await mysql.createConnection({ /* ... */ });
558
+ const executor = createMysqlExecutor(connection);
559
+ const orm = new Orm({
560
+ dialect: new MySqlDialect(),
561
+ executorFactory: {
562
+ createExecutor: () => executor,
563
+ createTransactionalExecutor: () => executor,
564
+ dispose: async () => {},
565
+ },
566
+ });
567
+ const session = new OrmSession({ orm, executor });
568
+
569
+ // 3) Query starting from the entity class
570
+ const U = entityRef(User);
571
+ const [user] = await selectFromEntity(User)
572
+ .select('id', 'name')
573
+ .includeLazy('posts')
574
+ .where(eq(U.id, 1))
575
+ .execute(session); // user is an actual instance of the User class!
576
+
577
+ // Use executePlain() if you want raw POJOs instead of class instances
578
+ // Return type is inferred from selected columns: { id: number; name: string }[]
579
+ const rawUsers = await selectFromEntity(User)
580
+ .select('id', 'name')
581
+ .executePlain(session);
582
+
583
+ // Use firstOrFail() to get a single record or throw if not found
584
+ const admin = await selectFromEntity(User)
585
+ .where(eq(U.role, 'admin'))
586
+ .firstOrFail(session); // throws Error('No results found') if no match
587
+
588
+ // firstOrFailPlain() works the same but returns a POJO
589
+ const adminPlain = await selectFromEntity(User)
590
+ .where(eq(U.role, 'admin'))
591
+ .firstOrFailPlain(session);
592
+
593
+ user.posts.add({ title: 'From decorators' });
594
+ await session.commit();
595
+ ```
596
+
597
+ Note: relation helpers like `add`/`attach` are only available on tracked entities returned by `execute(session)`. `executePlain()` returns POJOs without relation wrappers, with return types inferred from your `.select()` callsβ€”no manual casting needed. Make sure the primary key (e.g. `id`) is selected so relation adds can link correctly.
598
+
599
+ Tip: to keep selections terse, use `select`, `include` (with `columns`), or the `sel`/`esel` helpers instead of spelling `table.columns.*` over and over. By default, `selectFromEntity` selects all columns if you don't specify any.
600
+
601
+
602
+ This level is nice when:
603
+
604
+ - You want classes as your domain model, but don't want a separate schema DSL.
605
+ - You like decorators for explicit mapping but still want AST-first SQL and a disciplined runtime.
606
+
607
+ ---
608
+
609
+ <a id="when-to-use-which-level"></a>
610
+ ## When to use which level? πŸ€”
611
+
612
+ - **Query builder + hydration (Level 1)**
613
+ Great for reporting/analytics, existing codebases with their own models, and services that need strong SQL but minimal runtime magic.
614
+
615
+ - **ORM runtime (Level 2)**
616
+ Great for request-scoped application logic and domain modeling where lazy relations, cascades, and graph persistence pay off.
617
+
618
+ - **Decorator entities (Level 3)**
619
+ Great when you want class-based entities and decorators, but still want to keep the underlying architecture explicit and layered.
620
+
621
+ All three levels share the same schema, AST, and dialects, so you can mix them as needed and migrate gradually.
622
+
623
+ ---
624
+
625
+ <a id="design-notes"></a>
626
+ ## Design & Architecture πŸ—οΈ
627
+
628
+ MetalORM is built on solid software engineering principles and proven design patterns.
629
+
630
+ ### Architecture Layers
631
+
632
+ ```
633
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
634
+ β”‚ Your Application β”‚
635
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
636
+ β”‚
637
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
638
+ β”‚ β”‚ β”‚
639
+ β–Ό β–Ό β–Ό
640
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
641
+ β”‚ Level 1 β”‚ β”‚ Level 2 β”‚ β”‚ Level 3 β”‚
642
+ β”‚ Query │◄────── ORM │◄──────Decoratorsβ”‚
643
+ β”‚ Builder β”‚ β”‚ Runtime β”‚ β”‚ β”‚
644
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
645
+ β”‚ β”‚ β”‚
646
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
647
+ β–Ό
648
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
649
+ β”‚ SQL AST β”‚
650
+ β”‚ (Typed Nodes) β”‚
651
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
652
+ β–Ό
653
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
654
+ β”‚ Strategy Pattern: Dialects β”‚
655
+ β”‚ MySQL | PostgreSQL | SQLite | SQL Server β”‚
656
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
657
+ β–Ό
658
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
659
+ β”‚ Database β”‚
660
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
661
+ ```
662
+
663
+ ### Design Patterns
664
+
665
+ - **Strategy Pattern**: Pluggable dialects (MySQL, PostgreSQL, SQLite, SQL Server) and function renderers allow the same query to target different databases
666
+ - **Visitor Pattern**: AST traversal for SQL compilation and expression processing
667
+ - **Builder Pattern**: Fluent query builders (Select, Insert, Update, Delete) for constructing queries step-by-step
668
+ - **Factory Pattern**: Dialect factory and executor creation abstract instantiation logic
669
+ - **Unit of Work**: Change tracking and batch persistence in `OrmSession` coordinate all modifications
670
+ - **Identity Map**: One entity instance per row within a session prevents duplicate object issues
671
+ - **Interceptor/Pipeline**: Query interceptors and flush lifecycle hooks enable cross-cutting concerns
672
+ - **Adapter Pattern**: Connection pooling adapters allow different pool implementations
673
+
674
+ ### Type Safety
675
+
676
+ - **Zero `any` types**: The entire src codebase contains zero `any` typesβ€”every value is properly typed
677
+ - **100% typed public API**: Every public method, parameter, and return value is fully typed
678
+ - **Full type inference**: From schema definition through query building to result hydration
679
+ - **Compile-time safety**: Catch SQL errors at TypeScript compile time, not runtime
680
+ - **Generic-driven**: Leverages TypeScript generics extensively for type propagation
681
+
682
+ ### Separation of Concerns
683
+
684
+ Each layer has a clear, focused responsibility:
685
+
686
+ - **Core AST layer**: SQL representation independent of any specific dialect
687
+ - **Dialect layer**: Vendor-specific SQL compilation (MySQL, PostgreSQL, etc.)
688
+ - **Schema layer**: Table and column definitions with relations
689
+ - **Query builder layer**: Fluent API for building type-safe queries
690
+ - **Hydration layer**: Transforms flat result sets into nested object graphs
691
+ - **ORM runtime layer**: Entity management, change tracking, lazy relations, transactions
692
+
693
+ You can use just the layers you need and stay at the low level (AST + dialects) or adopt higher levels when beneficial.
694
+
695
+ ---
696
+
697
+ ## Frequently Asked Questions ❓
698
+
699
+ **Q: How does MetalORM differ from other ORMs?**
700
+ A: MetalORM's unique three-level architecture lets you choose your abstraction levelβ€”use just the query builder, add the ORM runtime when needed, or go full decorator-based entities. This gradual adoption path is uncommon in the TypeScript ecosystem. You're not locked into an all-or-nothing ORM approach.
701
+
702
+ **Q: Can I use this in production?**
703
+ A: Yes! MetalORM is designed for production use with robust patterns like Unit of Work, Identity Map, and connection pooling support. The type-safe query builder ensures SQL correctness at compile time.
704
+
705
+ **Q: Do I need to use all three levels?**
706
+ A: No! Use only what you need. Many projects stay at Level 1 (query builder) for its type-safe SQL building without any ORM overhead. Add runtime features (Level 2) or decorators (Level 3) only where they provide value.
707
+
708
+ **Q: What about migrations?**
709
+ A: MetalORM provides schema generation via DDL builders. See the [Schema Generation docs](./docs/schema-generation.md) for details on generating CREATE TABLE statements from your table definitions.
710
+
711
+ **Q: How type-safe is it really?**
712
+ A: Exceptionally. The entire codebase contains **zero** `any` typesβ€”every value is properly typed with TypeScript generics and inference. All public APIs are fully typed, and your queries, entities, and results get full TypeScript checking at compile time.
713
+
714
+ **Q: What design patterns are used?**
715
+ A: MetalORM implements several well-known patterns: Strategy (dialects & functions), Visitor (AST traversal), Builder (query construction), Factory (dialect & executor creation), Unit of Work (change tracking), Identity Map (entity caching), Interceptor (query hooks), and Adapter (pooling). This makes the codebase maintainable and extensible.
716
+
717
+ ---
718
+
719
+ ## Performance & Production πŸš€
720
+
721
+ - **Zero runtime overhead for Level 1** (query builder) - it's just SQL compilation and hydration
722
+ - **Efficient batching** for Level 2 lazy relations minimizes database round-trips
723
+ - **Identity Map** prevents duplicate entity instances and unnecessary queries
724
+ - **Connection pooling** supported via executor factory pattern (see [pooling docs](./docs/pooling.md))
725
+ - **Prepared statements** with parameterized queries protect against SQL injection
726
+
727
+ **Production checklist:**
728
+ - βœ… Use connection pooling for better resource management
729
+ - βœ… Enable query logging in development for debugging
730
+ - βœ… Set up proper error handling and retries
731
+ - βœ… Use transactions for multi-statement operations
732
+ - βœ… Monitor query performance with interceptors
733
+
734
+ ---
735
+
736
+ ## Community & Support πŸ’¬
737
+
738
+ - πŸ› **Issues:** [GitHub Issues](https://github.com/celsowm/metal-orm/issues)
739
+ - πŸ’‘ **Discussions:** [GitHub Discussions](https://github.com/celsowm/metal-orm/discussions)
740
+ - πŸ“– **Documentation:** [Full docs](./docs/index.md)
741
+ - πŸ—ΊοΈ **Roadmap:** [See what's planned](./ROADMAP.md)
742
+ - πŸ“¦ **Changelog:** [View releases](https://github.com/celsowm/metal-orm/releases)
743
+
744
+ ---
745
+
746
+ <a id="contributing"></a>
747
+ ## Contributing 🀝
748
+
749
+ Issues and PRs are welcome! If you're interested in pushing the runtime/ORM side further (soft deletes, multi-tenant filters, outbox patterns, etc.), contributions are especially appreciated.
750
+
751
+ See the contributing guide for details.
752
+
753
+ ---
754
+
755
+ <a id="license"></a>
756
+ ## License πŸ“„
757
+
758
+ MetalORM is MIT licensed.