metal-orm 1.0.0 → 1.0.2

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.
@@ -0,0 +1,38 @@
1
+ name: Publish metal-orm to npm
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write # OIDC for Trusted Publishing
11
+ packages: write
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Node
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: '20'
25
+ registry-url: 'https://registry.npmjs.org'
26
+
27
+ - name: Install deps
28
+ run: npm ci
29
+
30
+ - name: Test
31
+ run: npm test
32
+ continue-on-error: true # ou remove se quiser quebrar se falhar
33
+
34
+ - name: Build
35
+ run: npm run build
36
+
37
+ - name: Publish (Trusted Publishing)
38
+ run: npm publish --provenance --access public
package/README.md CHANGED
@@ -1,30 +1,93 @@
1
- <div align="center">
2
- <img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
3
- </div>
4
-
5
- # Run and deploy your AI Studio app
6
-
7
- This contains everything you need to run your app locally.
8
-
9
- View your app in AI Studio: https://ai.studio/apps/drive/1vXchpJ36cRXpwzrizwz6I-M1Ks1GAkz2
10
-
11
- ## Run Locally
12
-
13
- **Prerequisites:** Node.js
14
-
15
-
16
- 1. Install dependencies:
17
- `npm install`
18
- 2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
19
- 3. Run the app:
20
- `npm run dev`
21
-
22
- ## Project layout
23
-
24
- - `src/` hosts the React-based playground UI that backs the demo.
25
- - `src/metal-orm/src/` contains the real MetalORM implementation (AST, builder, dialects, runtime) consumed by the playground.
26
- - Legacy `orm/` and `services/orm/` directories were removed because they were unused duplicates, so future work belongs in `src/metal-orm/src`.
27
-
28
- ## Parameterized Queries
29
-
30
- Literal values in expressions are now emitted as parameter placeholders and collected in a binding list. Use `SelectQueryBuilder.compile(dialect)` to get `{ sql, params }` and pass both to your database driver (`client.executeSql(sql, params)`); `SelectQueryBuilder.toSql(dialect)` still returns just the SQL string for quick debugging.
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
+ **A TypeScript-first SQL query builder with schema-driven AST, hydrated relations, and multi-dialect compilation.**
8
+
9
+ MetalORM keeps SQL generation deterministic (CTEs, aggregates, window functions, EXISTS/subqueries) while letting you introspect the AST or reuse builders inside larger queries. It's designed for developers who want the power of raw SQL with the convenience of a modern ORM.
10
+
11
+ ## Documentation
12
+
13
+ For detailed information and API reference, please visit our [full documentation](docs/index.md).
14
+
15
+ ## Features
16
+
17
+ - **Declarative Schema Definition**: Define your database structure in TypeScript with full type inference.
18
+ - **Rich Query Building**: A fluent API to build simple and complex queries.
19
+ - **Advanced SQL Features**: Support for CTEs, window functions, subqueries, and more.
20
+ - **Relation Hydration**: Automatically transform flat database rows into nested JavaScript objects.
21
+ - **Multi-Dialect Support**: Compile the same query to different SQL dialects.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ # npm
27
+ npm install metal-orm
28
+
29
+ # yarn
30
+ yarn add metal-orm
31
+
32
+ # pnpm
33
+ pnpm add metal-orm
34
+ ```
35
+
36
+ ## Database Drivers
37
+
38
+ MetalORM compiles SQL; it does not bundle a database driver. Install one that matches your database, compile your query with the matching dialect, and hand the SQL + parameters to the driver.
39
+
40
+ | Dialect | Driver | Install |
41
+ | --- | --- | --- |
42
+ | MySQL / MariaDB | `mysql2` | `npm install mysql2` |
43
+ | SQLite | `sqlite3` | `npm install sqlite3` |
44
+ | SQL Server | `tedious` | `npm install tedious` |
45
+
46
+ After installing the driver, pick the matching dialect implementation before compiling the query (`MySqlDialect`, `SQLiteDialect`, `MSSQLDialect`), and execute the resulting SQL string with the driver of your choice.
47
+
48
+ ## Quick Start
49
+
50
+ ```typescript
51
+ import mysql from 'mysql2/promise';
52
+ import {
53
+ defineTable,
54
+ col,
55
+ SelectQueryBuilder,
56
+ eq,
57
+ hydrateRows,
58
+ MySqlDialect,
59
+ } from 'metal-orm';
60
+
61
+ const users = defineTable('users', {
62
+ id: col.int().primaryKey(),
63
+ name: col.varchar(255).notNull(),
64
+ });
65
+
66
+ const connection = await mysql.createConnection({
67
+ host: 'localhost',
68
+ user: 'root',
69
+ database: 'test',
70
+ });
71
+
72
+ const builder = new SelectQueryBuilder(users)
73
+ .select({
74
+ id: users.columns.id,
75
+ name: users.columns.name,
76
+ })
77
+ .where(eq(users.columns.id, 1));
78
+
79
+ const dialect = new MySqlDialect();
80
+ const { sql, params } = builder.compile(dialect);
81
+ const [rows] = await connection.execute(sql, params);
82
+ const hydrated = hydrateRows(rows as Record<string, unknown>[], builder.getHydrationPlan());
83
+
84
+ console.log(hydrated);
85
+ ```
86
+
87
+ ## Contributing
88
+
89
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for more details.
90
+
91
+ ## License
92
+
93
+ MetalORM is [MIT licensed](LICENSE).
@@ -0,0 +1,85 @@
1
+ # Advanced Features
2
+
3
+ MetalORM supports a wide range of advanced SQL features to handle complex scenarios.
4
+
5
+ ## Common Table Expressions (CTEs)
6
+
7
+ CTEs help organize complex queries. You can define a CTE using a `SelectQueryBuilder` and reference it in the main query.
8
+
9
+ ```typescript
10
+ const since = new Date();
11
+ since.setDate(since.getDate() - 30);
12
+
13
+ const activeUsers = new SelectQueryBuilder(users)
14
+ .selectRaw('*')
15
+ .where(gt(users.columns.lastLogin, since))
16
+ .as('active_users');
17
+
18
+ const query = new SelectQueryBuilder(activeUsers)
19
+ .with(activeUsers)
20
+ .selectRaw('*')
21
+ .where(eq(activeUsers.columns.id, 1));
22
+ ```
23
+
24
+ ## Window Functions
25
+
26
+ MetalORM provides helpers for window functions like `RANK()`, `ROW_NUMBER()`, etc.
27
+
28
+ ```typescript
29
+ const rankedPosts = new SelectQueryBuilder(posts)
30
+ .select({
31
+ id: posts.columns.id,
32
+ title: posts.columns.title,
33
+ rank: windowFunction('RANK', [], [posts.columns.userId], [
34
+ { column: posts.columns.createdAt, direction: 'DESC' }
35
+ ])
36
+ });
37
+ ```
38
+
39
+ ## Subqueries and EXISTS
40
+
41
+ You can use subqueries and `EXISTS` to perform complex checks.
42
+
43
+ ```typescript
44
+ const usersWithPosts = new SelectQueryBuilder(users)
45
+ .selectRaw('*')
46
+ .where(exists(
47
+ new SelectQueryBuilder(posts)
48
+ .selectRaw('1')
49
+ .where(eq(posts.columns.userId, users.columns.id))
50
+ ));
51
+ ```
52
+
53
+ ## JSON Operations
54
+
55
+ MetalORM provides helpers for working with JSON data.
56
+
57
+ ```typescript
58
+ const userData = defineTable('user_data', {
59
+ id: col.int().primaryKey(),
60
+ userId: col.int().notNull(),
61
+ preferences: col.json().notNull()
62
+ });
63
+
64
+ const jsonQuery = new SelectQueryBuilder(userData)
65
+ .select({
66
+ id: userData.columns.id,
67
+ theme: jsonPath(userData.columns.preferences, '$.theme')
68
+ })
69
+ .where(eq(jsonPath(userData.columns.preferences, '$.theme'), 'dark'));
70
+ ```
71
+
72
+ ## CASE Expressions
73
+
74
+ You can use `caseWhen()` to create `CASE` expressions for conditional logic.
75
+
76
+ ```typescript
77
+ const tieredUsers = new SelectQueryBuilder(users)
78
+ .select({
79
+ id: users.columns.id,
80
+ tier: caseWhen([
81
+ { when: gt(count(posts.columns.id), 10), then: 'power user' }
82
+ ], 'regular')
83
+ })
84
+ .groupBy(users.columns.id);
85
+ ```
@@ -0,0 +1,22 @@
1
+ # API Reference
2
+
3
+ This section provides a reference for the core classes, key functions, and utility functions in MetalORM.
4
+
5
+ ### Core Classes
6
+ - `SelectQueryBuilder` - Main query builder class
7
+ - `MySqlDialect` / `SQLiteDialect` / `MSSQLDialect` - SQL dialect compilers
8
+ - `HydrationManager` - Handles relation hydration logic
9
+
10
+ ### Key Functions
11
+ - `defineTable()` - Define database tables
12
+ - `col.*()` - Column type definitions
13
+ - `hasMany()` / `belongsTo()` - Relation definitions
14
+ - `eq()`, `and()`, `or()`, etc. - Expression builders
15
+ - `hydrateRows()` - Transform flat rows to nested objects
16
+
17
+ ### Utility Functions
18
+ - `count()`, `sum()`, `avg()` - Aggregate functions
19
+ - `like()`, `between()`, `inList()`, `notInList()` - Comparison operators
20
+ - `jsonPath()` - JSON extraction
21
+ - `caseWhen()`, `exists()`, `notExists()` - Conditional and subquery helpers
22
+ - `rowNumber()`, `rank()`, `denseRank()`, `lag()`, `lead()`, `windowFunction()` - Window function helpers
@@ -0,0 +1,104 @@
1
+ # Getting Started
2
+
3
+ This guide will help you get started with MetalORM.
4
+
5
+ ## Installation
6
+
7
+ You can install MetalORM using npm, yarn, or pnpm:
8
+
9
+ ```bash
10
+ # npm
11
+ npm install metal-orm
12
+
13
+ # yarn
14
+ yarn add metal-orm
15
+
16
+ # pnpm
17
+ pnpm add metal-orm
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ Here's a complete example to get you started:
23
+
24
+ ```typescript
25
+ import {
26
+ defineTable,
27
+ col,
28
+ hasMany,
29
+ SelectQueryBuilder,
30
+ eq,
31
+ count,
32
+ hydrateRows,
33
+ MySqlDialect
34
+ } from 'metal-orm';
35
+
36
+ // 1. Define your schema
37
+ const posts = defineTable(
38
+ 'posts',
39
+ {
40
+ id: col.int().primaryKey(),
41
+ title: col.varchar(255).notNull(),
42
+ content: col.text(),
43
+ userId: col.int().notNull(),
44
+ createdAt: col.timestamp().default('CURRENT_TIMESTAMP'),
45
+ updatedAt: col.timestamp()
46
+ }
47
+ );
48
+
49
+ const users = defineTable(
50
+ 'users',
51
+ {
52
+ id: col.int().primaryKey(),
53
+ name: col.varchar(255).notNull(),
54
+ email: col.varchar(255).unique(),
55
+ createdAt: col.timestamp().default('CURRENT_TIMESTAMP')
56
+ },
57
+ {
58
+ posts: hasMany(posts, 'userId')
59
+ }
60
+ );
61
+
62
+ // 2. Build your query
63
+ const builder = new SelectQueryBuilder(users)
64
+ .select({
65
+ id: users.columns.id,
66
+ name: users.columns.name,
67
+ email: users.columns.email,
68
+ totalPosts: count(posts.columns.id)
69
+ })
70
+ .leftJoin(posts, eq(posts.columns.userId, users.columns.id))
71
+ .groupBy(users.columns.id, users.columns.name, users.columns.email)
72
+ .orderBy(count(posts.columns.id), 'DESC')
73
+ .limit(20)
74
+ .include('posts', {
75
+ columns: [posts.columns.id, posts.columns.title, posts.columns.createdAt]
76
+ });
77
+
78
+ // 3. Compile to SQL
79
+ const dialect = new MySqlDialect();
80
+ const { sql, params } = builder.compile(dialect);
81
+
82
+ // 4. Execute and hydrate
83
+ const rows = await connection.execute(sql, params);
84
+ const hydrated = hydrateRows(rows, builder.getHydrationPlan());
85
+
86
+ console.log(hydrated);
87
+ // [
88
+ // {
89
+ // id: 1,
90
+ // name: 'John Doe',
91
+ // email: 'john@example.com',
92
+ // totalPosts: 15,
93
+ // posts: [
94
+ // {
95
+ // id: 101,
96
+ // title: 'Latest Post',
97
+ // createdAt: '2023-05-15T10:00:00.000Z'
98
+ // },
99
+ // // ... more posts
100
+ // ]
101
+ // }
102
+ // // ... more users
103
+ // ]
104
+ ```
@@ -0,0 +1,41 @@
1
+ # Relation Hydration
2
+
3
+ MetalORM can automatically transform flat database rows into nested JavaScript objects, making it easy to work with relational data.
4
+
5
+ ## Hydrating Results
6
+
7
+ The `hydrateRows()` function takes an array of database rows and a hydration plan to produce nested objects. The hydration plan is generated by the `SelectQueryBuilder` when you use the `include()` method.
8
+
9
+ ```typescript
10
+ const builder = new SelectQueryBuilder(users)
11
+ .selectRaw('*')
12
+ .include('posts', {
13
+ columns: ['id', 'title', 'content'],
14
+ include: {
15
+ comments: {
16
+ columns: ['id', 'content', 'createdAt']
17
+ }
18
+ }
19
+ });
20
+
21
+ const { sql, params } = builder.compile(new MySqlDialect());
22
+ const rows = await db.execute(sql, params);
23
+
24
+ // Automatically hydrates to:
25
+ // {
26
+ // id: 1,
27
+ // name: 'John',
28
+ // posts: [
29
+ // {
30
+ // id: 1,
31
+ // title: 'First Post',
32
+ // comments: [...]
33
+ // }
34
+ // ]
35
+ // }
36
+ const hydrated = hydrateRows(rows, builder.getHydrationPlan());
37
+ ```
38
+
39
+ ## How it Works
40
+
41
+ The `SelectQueryBuilder` analyzes the `include()` configuration and generates a `HydrationPlan`. This plan contains the necessary information to map the flat rows to a nested structure, including relation details and column aliases. The `hydrateRows()` function then uses this plan to efficiently process the result set.
package/docs/index.md ADDED
@@ -0,0 +1,31 @@
1
+ # Introduction to MetalORM
2
+
3
+ MetalORM is a TypeScript-first SQL query builder designed for developers who want the power of raw SQL with the convenience of a modern ORM. It keeps SQL generation deterministic (CTEs, aggregates, window functions, EXISTS/subqueries) while letting you introspect the AST or reuse builders inside larger queries.
4
+
5
+ ## Philosophy
6
+
7
+ MetalORM follows these core principles:
8
+
9
+ - **Type Safety First**: Leverage TypeScript to catch errors at compile time
10
+ - **SQL Transparency**: Generate predictable, readable SQL that you can inspect
11
+ - **Composition Over Configuration**: Build complex queries by composing simple parts
12
+ - **Zero Magic**: Explicit operations with clear AST representation
13
+ - **Multi-Dialect Support**: Write once, compile to MySQL, SQLite, or SQL Server
14
+
15
+ ## Features
16
+
17
+ - **Declarative Schema Definition**: Define your database structure in TypeScript with full type inference.
18
+ - **Rich Query Building**: A fluent API to build simple and complex queries.
19
+ - **Advanced SQL Features**: Support for CTEs, window functions, subqueries, and more.
20
+ - **Relation Hydration**: Automatically transform flat database rows into nested JavaScript objects.
21
+ - **Multi-Dialect Support**: Compile the same query to different SQL dialects.
22
+
23
+ ## Table of Contents
24
+
25
+ - [Getting Started](./getting-started.md)
26
+ - [Schema Definition](./schema-definition.md)
27
+ - [Query Builder](./query-builder.md)
28
+ - [Advanced Features](./advanced-features.md)
29
+ - [Hydration](./hydration.md)
30
+ - [Multi-Dialect Support](./multi-dialect-support.md)
31
+ - [API Reference](./api-reference.md)
@@ -0,0 +1,34 @@
1
+ # Multi-Dialect Support
2
+
3
+ MetalORM is designed to be database-agnostic. You can write your queries once and compile them to different SQL dialects.
4
+
5
+ ## Compiling Queries
6
+
7
+ The `compile()` method on the `SelectQueryBuilder` takes a dialect instance and returns the compiled SQL and parameters.
8
+
9
+ ```typescript
10
+ const query = new SelectQueryBuilder(users)
11
+ .selectRaw('*')
12
+ .where(eq(users.columns.id, 1))
13
+ .limit(10);
14
+
15
+ // MySQL
16
+ const mysql = query.compile(new MySqlDialect());
17
+ // SQL: SELECT * FROM users WHERE id = ? LIMIT ?
18
+
19
+ // SQLite
20
+ const sqlite = query.compile(new SQLiteDialect());
21
+ // SQL: SELECT * FROM users WHERE id = ? LIMIT ?
22
+
23
+ // SQL Server
24
+ const mssql = query.compile(new MSSQLDialect());
25
+ // SQL: SELECT TOP 10 * FROM users WHERE id = @p1
26
+ ```
27
+
28
+ ## Supported Dialects
29
+
30
+ - **MySQL**: `MySqlDialect`
31
+ - **SQLite**: `SQLiteDialect`
32
+ - **SQL Server**: `MSSQLDialect`
33
+
34
+ Each dialect handles the specific syntax and parameterization of the target database.
@@ -0,0 +1,75 @@
1
+ # Query Builder
2
+
3
+ MetalORM's query builder provides a fluent and expressive API for constructing SQL queries.
4
+
5
+ ## Selecting Data
6
+
7
+ The `SelectQueryBuilder` is the main entry point for building `SELECT` queries.
8
+
9
+ ### Basic Selections
10
+
11
+ You can select all columns using `selectRaw('*')` or specify columns using `select()`:
12
+
13
+ ```typescript
14
+ // Select all columns
15
+ const query = new SelectQueryBuilder(users).selectRaw('*');
16
+
17
+ // Select specific columns
18
+ const query = new SelectQueryBuilder(users).select({
19
+ id: users.columns.id,
20
+ name: users.columns.name,
21
+ });
22
+ ```
23
+
24
+ ### Joins
25
+
26
+ You can join tables using `leftJoin`, `innerJoin`, `rightJoin`, etc.
27
+
28
+ ```typescript
29
+ const query = new SelectQueryBuilder(users)
30
+ .select({
31
+ userId: users.columns.id,
32
+ postTitle: posts.columns.title,
33
+ })
34
+ .leftJoin(posts, eq(posts.columns.userId, users.columns.id));
35
+ ```
36
+
37
+ ### Filtering
38
+
39
+ You can filter results using the `where()` method with expression helpers:
40
+
41
+ ```typescript
42
+ const query = new SelectQueryBuilder(users)
43
+ .selectRaw('*')
44
+ .where(and(
45
+ like(users.columns.name, '%John%'),
46
+ gt(users.columns.createdAt, new Date('2023-01-01'))
47
+ ));
48
+ ```
49
+
50
+ ### Aggregation
51
+
52
+ You can use aggregate functions like `count()`, `sum()`, `avg()`, etc., and group the results.
53
+
54
+ ```typescript
55
+ const query = new SelectQueryBuilder(users)
56
+ .select({
57
+ userId: users.columns.id,
58
+ postCount: count(posts.columns.id),
59
+ })
60
+ .leftJoin(posts, eq(posts.columns.userId, users.columns.id))
61
+ .groupBy(users.columns.id)
62
+ .having(gt(count(posts.columns.id), 5));
63
+ ```
64
+
65
+ ### Ordering and Pagination
66
+
67
+ You can order the results using `orderBy()` and paginate using `limit()` and `offset()`.
68
+
69
+ ```typescript
70
+ const query = new SelectQueryBuilder(posts)
71
+ .selectRaw('*')
72
+ .orderBy(posts.columns.createdAt, 'DESC')
73
+ .limit(10)
74
+ .offset(20);
75
+ ```
@@ -0,0 +1,61 @@
1
+ # Schema Definition
2
+
3
+ MetalORM allows you to define your database schema in TypeScript, providing full type inference and a single source of truth for your data structures.
4
+
5
+ ## Defining Tables
6
+
7
+ You can define a table using the `defineTable` function. It takes the table name, a columns object, and an optional relations object.
8
+
9
+ ```typescript
10
+ import { defineTable, col } from 'metal-orm';
11
+
12
+ const users = defineTable('users', {
13
+ id: col.int().primaryKey(),
14
+ name: col.varchar(255).notNull(),
15
+ email: col.varchar(255).unique(),
16
+ createdAt: col.timestamp().default('CURRENT_TIMESTAMP'),
17
+ });
18
+ ```
19
+
20
+ ## Column Types
21
+
22
+ MetalORM provides a variety of column types through the `col` object:
23
+
24
+ - `col.int()`: Integer
25
+ - `col.varchar(length)`: Variable-length string
26
+ - `col.text()`: Text
27
+ - `col.timestamp()`: Timestamp
28
+ - `col.json()`: JSON
29
+ - ...and more.
30
+
31
+ You can also chain modifiers to define column constraints:
32
+
33
+ - `.primaryKey()`: Marks the column as a primary key.
34
+ - `.notNull()`: Adds a `NOT NULL` constraint.
35
+ - `.unique()`: Adds a `UNIQUE` constraint.
36
+ - `.default(value)`: Sets a default value.
37
+
38
+ ## Relations
39
+
40
+ You can define relations between tables using `hasMany` and `belongsTo`:
41
+
42
+ ```typescript
43
+ import { defineTable, col, hasMany } from 'metal-orm';
44
+
45
+ const posts = defineTable('posts', {
46
+ id: col.int().primaryKey(),
47
+ title: col.varchar(255).notNull(),
48
+ userId: col.int().notNull(),
49
+ });
50
+
51
+ const users = defineTable(
52
+ 'users',
53
+ {
54
+ id: col.int().primaryKey(),
55
+ name: col.varchar(255).notNull(),
56
+ },
57
+ {
58
+ posts: hasMany(posts, 'userId'),
59
+ }
60
+ );
61
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -42,4 +42,4 @@
42
42
  "vite": "^5.0.0",
43
43
  "vitest": "^4.0.14"
44
44
  }
45
- }
45
+ }
@@ -51,7 +51,8 @@ export type OrderDirection = (typeof ORDER_DIRECTIONS)[keyof typeof ORDER_DIRECT
51
51
  export const SUPPORTED_DIALECTS = {
52
52
  MYSQL: 'mysql',
53
53
  SQLITE: 'sqlite',
54
- MSSQL: 'mssql'
54
+ MSSQL: 'mssql',
55
+ POSTGRES: 'postgres'
55
56
  } as const;
56
57
 
57
58
  export type DialectName = (typeof SUPPORTED_DIALECTS)[keyof typeof SUPPORTED_DIALECTS];
@@ -0,0 +1,81 @@
1
+ import { CompilerContext, Dialect } from '../abstract';
2
+ import { SelectQueryNode } from '../../ast/query';
3
+ import { JsonPathNode } from '../../ast/expression';
4
+
5
+ export class PostgresDialect extends Dialect {
6
+ public constructor() {
7
+ super();
8
+ }
9
+
10
+ quoteIdentifier(id: string): string {
11
+ return `"${id}"`;
12
+ }
13
+
14
+ protected compileJsonPath(node: JsonPathNode): string {
15
+ const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
16
+ // Postgres uses col->>'path' for text extraction
17
+ return `${col}->>'${node.path}'`;
18
+ }
19
+
20
+ protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
21
+ const columns = ast.columns.map(c => {
22
+ let expr = '';
23
+ if (c.type === 'Function') {
24
+ expr = this.compileOperand(c, ctx);
25
+ } else if (c.type === 'Column') {
26
+ expr = `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`;
27
+ } else if (c.type === 'ScalarSubquery') {
28
+ expr = this.compileOperand(c, ctx);
29
+ } else if (c.type === 'WindowFunction') {
30
+ expr = this.compileOperand(c, ctx);
31
+ }
32
+
33
+ if (c.alias) {
34
+ if (c.alias.includes('(')) return c.alias;
35
+ return `${expr} AS ${this.quoteIdentifier(c.alias)}`;
36
+ }
37
+ return expr;
38
+ }).join(', ');
39
+
40
+ const distinct = ast.distinct ? 'DISTINCT ' : '';
41
+ const from = `${this.quoteIdentifier(ast.from.name)}`;
42
+
43
+ const joins = ast.joins.map(j => {
44
+ const table = this.quoteIdentifier(j.table.name);
45
+ const cond = this.compileExpression(j.condition, ctx);
46
+ return `${j.kind} JOIN ${table} ON ${cond}`;
47
+ }).join(' ');
48
+ const whereClause = this.compileWhere(ast.where, ctx);
49
+
50
+ const groupBy = ast.groupBy && ast.groupBy.length > 0
51
+ ? ' GROUP BY ' + ast.groupBy.map(c => `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`).join(', ')
52
+ : '';
53
+
54
+ const having = ast.having
55
+ ? ` HAVING ${this.compileExpression(ast.having, ctx)}`
56
+ : '';
57
+
58
+ const orderBy = ast.orderBy && ast.orderBy.length > 0
59
+ ? ' ORDER BY ' + ast.orderBy.map(o => `${this.quoteIdentifier(o.column.table)}.${this.quoteIdentifier(o.column.name)} ${o.direction}`).join(', ')
60
+ : '';
61
+
62
+ const limit = ast.limit ? ` LIMIT ${ast.limit}` : '';
63
+ const offset = ast.offset ? ` OFFSET ${ast.offset}` : '';
64
+
65
+ const ctes = ast.ctes && ast.ctes.length > 0
66
+ ? (() => {
67
+ const hasRecursive = ast.ctes.some(cte => cte.recursive);
68
+ const prefix = hasRecursive ? 'WITH RECURSIVE ' : 'WITH ';
69
+ const cteDefs = ast.ctes.map(cte => {
70
+ const name = this.quoteIdentifier(cte.name);
71
+ const cols = cte.columns ? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})` : '';
72
+ const query = this.compileSelectAst(cte.query, ctx).trim().replace(/;$/, '');
73
+ return `${name}${cols} AS (${query})`;
74
+ }).join(', ');
75
+ return prefix + cteDefs + ' ';
76
+ })()
77
+ : '';
78
+
79
+ return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}${limit}${offset};`;
80
+ }
81
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SelectQueryBuilder } from '../src/builder/select';
3
+ import { PostgresDialect } from '../src/dialect/postgres';
4
+ import { Users } from '../src/playground/features/playground/data/schema';
5
+ import { jsonPath, eq } from '../src/ast/expression';
6
+
7
+ describe('PostgresDialect', () => {
8
+ it('should compile a simple select', () => {
9
+ const query = new SelectQueryBuilder(Users).selectRaw('*');
10
+ const dialect = new PostgresDialect();
11
+ const compiled = query.compile(dialect);
12
+ expect(compiled.sql).toBe('SELECT "users"."*" FROM "users";');
13
+ });
14
+
15
+ it('should compile a select with a where clause', () => {
16
+ const query = new SelectQueryBuilder(Users).selectRaw('*').where(eq(Users.columns.id, 1));
17
+ const dialect = new PostgresDialect();
18
+ const compiled = query.compile(dialect);
19
+ expect(compiled.sql).toBe('SELECT "users"."*" FROM "users" WHERE "users"."id" = ?;');
20
+ expect(compiled.params).toEqual([1]);
21
+ });
22
+
23
+ it('should compile a select with a json path', () => {
24
+ const query = new SelectQueryBuilder(Users).selectRaw('*').where(eq(jsonPath(Users.columns.settings, '$.first'), 'John'));
25
+ const dialect = new PostgresDialect();
26
+ const compiled = query.compile(dialect);
27
+ expect(compiled.sql).toBe('SELECT "users"."*" FROM "users" WHERE "users"."settings"->>\'$.first\' = ?;');
28
+ expect(compiled.params).toEqual(['John']);
29
+ });
30
+ });
@@ -3,6 +3,7 @@ import { SelectQueryBuilder } from '../src/builder/select';
3
3
  import { SqliteDialect } from '../src/dialect/sqlite';
4
4
  import { MySqlDialect } from '../src/dialect/mysql';
5
5
  import { SqlServerDialect } from '../src/dialect/mssql';
6
+ import { PostgresDialect } from '../src/dialect/postgres';
6
7
  import { TableDef } from '../src/schema/table';
7
8
  import { rowNumber, rank, denseRank, lag, lead, ntile, firstValue, lastValue, windowFunction } from '../src/ast/expression';
8
9
 
@@ -14,6 +15,7 @@ describe('Window Function Support', () => {
14
15
  const sqlite = new SqliteDialect();
15
16
  const mysql = new MySqlDialect();
16
17
  const mssql = new SqlServerDialect();
18
+ const postgres = new PostgresDialect();
17
19
 
18
20
  it('should generate ROW_NUMBER() window function', () => {
19
21
  const users = table('users');
@@ -28,10 +30,12 @@ describe('Window Function Support', () => {
28
30
  const expectedSqlite = 'SELECT "users"."id" AS "id", "users"."name" AS "name", ROW_NUMBER() OVER () AS "row_num" FROM "users";';
29
31
  const expectedMysql = 'SELECT `users`.`id` AS `id`, `users`.`name` AS `name`, ROW_NUMBER() OVER () AS `row_num` FROM `users`;';
30
32
  const expectedMssql = 'SELECT [users].[id] AS [id], [users].[name] AS [name], ROW_NUMBER() OVER () AS [row_num] FROM [users];';
33
+ const expectedPostgres = 'SELECT "users"."id" AS "id", "users"."name" AS "name", ROW_NUMBER() OVER () AS "row_num" FROM "users";';
31
34
 
32
35
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
33
36
  expect(query.toSql(mysql)).toBe(expectedMysql);
34
37
  expect(query.toSql(mssql)).toBe(expectedMssql);
38
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
35
39
  });
36
40
 
37
41
  it('should generate RANK() with PARTITION BY and ORDER BY', () => {
@@ -48,10 +52,12 @@ describe('Window Function Support', () => {
48
52
  const expectedSqlite = 'SELECT "orders"."id" AS "id", "orders"."customer_id" AS "customer_id", "orders"."amount" AS "amount", RANK() OVER (PARTITION BY "orders"."customer_id" ORDER BY "orders"."amount" DESC) AS "rank" FROM "orders";';
49
53
  const expectedMysql = 'SELECT `orders`.`id` AS `id`, `orders`.`customer_id` AS `customer_id`, `orders`.`amount` AS `amount`, RANK() OVER (PARTITION BY `orders`.`customer_id` ORDER BY `orders`.`amount` DESC) AS `rank` FROM `orders`;';
50
54
  const expectedMssql = 'SELECT [orders].[id] AS [id], [orders].[customer_id] AS [customer_id], [orders].[amount] AS [amount], RANK() OVER (PARTITION BY [orders].[customer_id] ORDER BY [orders].[amount] DESC) AS [rank] FROM [orders];';
55
+ const expectedPostgres = 'SELECT "orders"."id" AS "id", "orders"."customer_id" AS "customer_id", "orders"."amount" AS "amount", RANK() OVER (PARTITION BY "orders"."customer_id" ORDER BY "orders"."amount" DESC) AS "rank" FROM "orders";';
51
56
 
52
57
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
53
58
  expect(query.toSql(mysql)).toBe(expectedMysql);
54
59
  expect(query.toSql(mssql)).toBe(expectedMssql);
60
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
55
61
  });
56
62
 
57
63
  it('should generate LAG function with offset and default value', () => {
@@ -67,10 +73,12 @@ describe('Window Function Support', () => {
67
73
  const expectedSqlite = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LAG("sales"."amount", ?, ?) OVER () AS "prev_amount" FROM "sales";';
68
74
  const expectedMysql = 'SELECT `sales`.`date` AS `date`, `sales`.`amount` AS `amount`, LAG(`sales`.`amount`, ?, ?) OVER () AS `prev_amount` FROM `sales`;';
69
75
  const expectedMssql = 'SELECT [sales].[date] AS [date], [sales].[amount] AS [amount], LAG([sales].[amount], @p1, @p2) OVER () AS [prev_amount] FROM [sales];';
76
+ const expectedPostgres = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LAG("sales"."amount", ?, ?) OVER () AS "prev_amount" FROM "sales";';
70
77
 
71
78
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
72
79
  expect(query.toSql(mysql)).toBe(expectedMysql);
73
80
  expect(query.toSql(mssql)).toBe(expectedMssql);
81
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
74
82
  });
75
83
 
76
84
  it('should generate LEAD function', () => {
@@ -86,10 +94,12 @@ describe('Window Function Support', () => {
86
94
  const expectedSqlite = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LEAD("sales"."amount", ?) OVER () AS "next_amount" FROM "sales";';
87
95
  const expectedMysql = 'SELECT `sales`.`date` AS `date`, `sales`.`amount` AS `amount`, LEAD(`sales`.`amount`, ?) OVER () AS `next_amount` FROM `sales`;';
88
96
  const expectedMssql = 'SELECT [sales].[date] AS [date], [sales].[amount] AS [amount], LEAD([sales].[amount], @p1) OVER () AS [next_amount] FROM [sales];';
97
+ const expectedPostgres = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LEAD("sales"."amount", ?) OVER () AS "next_amount" FROM "sales";';
89
98
 
90
99
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
91
100
  expect(query.toSql(mysql)).toBe(expectedMysql);
92
101
  expect(query.toSql(mssql)).toBe(expectedMssql);
102
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
93
103
  });
94
104
 
95
105
  it('should generate window function with both PARTITION BY and ORDER BY', () => {
@@ -107,10 +117,12 @@ describe('Window Function Support', () => {
107
117
  const expectedSqlite = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."department" AS "department", "employees"."salary" AS "salary", ROW_NUMBER() OVER (PARTITION BY "employees"."department" ORDER BY "employees"."salary" DESC) AS "dept_rank" FROM "employees";';
108
118
  const expectedMysql = 'SELECT `employees`.`id` AS `id`, `employees`.`name` AS `name`, `employees`.`department` AS `department`, `employees`.`salary` AS `salary`, ROW_NUMBER() OVER (PARTITION BY `employees`.`department` ORDER BY `employees`.`salary` DESC) AS `dept_rank` FROM `employees`;';
109
119
  const expectedMssql = 'SELECT [employees].[id] AS [id], [employees].[name] AS [name], [employees].[department] AS [department], [employees].[salary] AS [salary], ROW_NUMBER() OVER (PARTITION BY [employees].[department] ORDER BY [employees].[salary] DESC) AS [dept_rank] FROM [employees];';
120
+ const expectedPostgres = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."department" AS "department", "employees"."salary" AS "salary", ROW_NUMBER() OVER (PARTITION BY "employees"."department" ORDER BY "employees"."salary" DESC) AS "dept_rank" FROM "employees";';
110
121
 
111
122
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
112
123
  expect(query.toSql(mysql)).toBe(expectedMysql);
113
124
  expect(query.toSql(mssql)).toBe(expectedMssql);
125
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
114
126
  });
115
127
 
116
128
  it('should generate multiple window functions in one query', () => {
@@ -129,9 +141,11 @@ describe('Window Function Support', () => {
129
141
  const expectedSqlite = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."salary" AS "salary", ROW_NUMBER() OVER () AS "row_num", RANK() OVER () AS "rank", DENSE_RANK() OVER () AS "dense_rank" FROM "employees";';
130
142
  const expectedMysql = 'SELECT `employees`.`id` AS `id`, `employees`.`name` AS `name`, `employees`.`salary` AS `salary`, ROW_NUMBER() OVER () AS `row_num`, RANK() OVER () AS `rank`, DENSE_RANK() OVER () AS `dense_rank` FROM `employees`;';
131
143
  const expectedMssql = 'SELECT [employees].[id] AS [id], [employees].[name] AS [name], [employees].[salary] AS [salary], ROW_NUMBER() OVER () AS [row_num], RANK() OVER () AS [rank], DENSE_RANK() OVER () AS [dense_rank] FROM [employees];';
144
+ const expectedPostgres = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."salary" AS "salary", ROW_NUMBER() OVER () AS "row_num", RANK() OVER () AS "rank", DENSE_RANK() OVER () AS "dense_rank" FROM "employees";';
132
145
 
133
146
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
134
147
  expect(query.toSql(mysql)).toBe(expectedMysql);
135
148
  expect(query.toSql(mssql)).toBe(expectedMssql);
149
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
136
150
  });
137
151
  });