metal-orm 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +341 -146
- package/dist/decorators/index.cjs +2564 -0
- package/dist/decorators/index.cjs.map +1 -0
- package/dist/decorators/index.d.cts +53 -0
- package/dist/decorators/index.d.ts +53 -0
- package/dist/decorators/index.js +2530 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/index.cjs +4227 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +701 -0
- package/dist/index.d.ts +701 -0
- package/dist/index.js +4131 -0
- package/dist/index.js.map +1 -0
- package/dist/select-654m4qy8.d.cts +1522 -0
- package/dist/select-654m4qy8.d.ts +1522 -0
- package/package.json +27 -20
- package/src/codegen/typescript.ts +405 -393
- package/src/core/ast/aggregate-functions.ts +30 -0
- package/src/core/ast/builders.ts +43 -0
- package/src/core/ast/expression-builders.ts +310 -0
- package/src/core/ast/expression-nodes.ts +211 -0
- package/src/core/ast/expression-visitor.ts +99 -0
- package/src/core/ast/expression.ts +5 -0
- package/src/{utils → core/ast}/join-node.ts +20 -20
- package/src/{ast → core/ast}/join.ts +18 -18
- package/src/{ast → core/ast}/query.ts +113 -113
- package/src/core/ast/window-functions.ts +140 -0
- package/src/{dialect → core/dialect}/abstract.ts +94 -94
- package/src/{dialect → core/dialect}/mssql/index.ts +31 -31
- package/src/{dialect → core/dialect}/mysql/index.ts +31 -31
- package/src/{dialect → core/dialect}/postgres/index.ts +45 -45
- package/src/{dialect → core/dialect}/sqlite/index.ts +45 -45
- package/src/{constants → core/sql}/sql-operator-config.ts +39 -39
- package/src/decorators/bootstrap.ts +126 -0
- package/src/decorators/column.ts +78 -0
- package/src/decorators/entity.ts +36 -0
- package/src/decorators/index.ts +4 -0
- package/src/decorators/relations.ts +107 -0
- package/src/global.d.ts +1 -0
- package/src/index.ts +22 -22
- package/src/orm/db-executor.ts +11 -0
- package/src/orm/domain-event-bus.ts +52 -0
- package/src/{runtime → orm}/entity-meta.ts +52 -52
- package/src/orm/entity-metadata.ts +140 -0
- package/src/{runtime → orm}/entity.ts +252 -252
- package/src/{runtime → orm}/execute.ts +36 -36
- package/src/{runtime → orm}/hydration.ts +103 -103
- package/src/orm/identity-map.ts +37 -0
- package/src/{runtime → orm}/lazy-batch.ts +205 -205
- package/src/orm/orm-context.ts +154 -0
- package/src/orm/relation-change-processor.ts +140 -0
- package/src/{runtime → orm}/relations/belongs-to.ts +92 -92
- package/src/{runtime → orm}/relations/has-many.ts +111 -111
- package/src/{runtime → orm}/relations/many-to-many.ts +149 -149
- package/src/orm/runtime-types.ts +39 -0
- package/src/orm/transaction-runner.ts +17 -0
- package/src/orm/unit-of-work.ts +232 -0
- package/src/{builder/operations → query-builder}/column-selector.ts +78 -78
- package/src/{builder → query-builder}/delete-query-state.ts +38 -42
- package/src/{builder → query-builder}/delete.ts +46 -57
- package/src/{builder → query-builder}/hydration-manager.ts +87 -87
- package/src/{builder → query-builder}/hydration-planner.ts +182 -182
- package/src/{builder → query-builder}/insert-query-state.ts +51 -62
- package/src/{builder → query-builder}/insert.ts +48 -59
- package/src/{builder → query-builder}/query-ast-service.ts +208 -226
- package/src/{utils → query-builder}/raw-column-parser.ts +32 -32
- package/src/{builder → query-builder}/relation-conditions.ts +112 -112
- package/src/{builder/operations → query-builder}/relation-manager.ts +82 -82
- package/src/{builder → query-builder}/relation-projection-helper.ts +101 -101
- package/src/{builder → query-builder}/relation-service.ts +284 -284
- package/src/{builder → query-builder}/relation-types.ts +21 -21
- package/src/{builder → query-builder}/relation-utils.ts +12 -12
- package/src/{builder → query-builder}/select-query-builder-deps.ts +112 -94
- package/src/{builder → query-builder}/select-query-state.ts +179 -179
- package/src/{builder → query-builder}/select.ts +78 -69
- package/src/{builder → query-builder}/update-query-state.ts +55 -59
- package/src/{builder → query-builder}/update.ts +50 -61
- package/src/schema/column.ts +25 -25
- package/src/schema/relation.ts +116 -116
- package/src/schema/table.ts +34 -34
- package/src/schema/types.ts +76 -76
- package/.github/workflows/publish-metal-orm.yml +0 -38
- package/ROADMAP.md +0 -125
- package/docs/CHANGES.md +0 -104
- package/docs/advanced-features.md +0 -176
- package/docs/api-reference.md +0 -31
- package/docs/dml-operations.md +0 -156
- package/docs/getting-started.md +0 -171
- package/docs/hydration.md +0 -115
- package/docs/index.md +0 -36
- package/docs/multi-dialect-support.md +0 -59
- package/docs/query-builder.md +0 -135
- package/docs/runtime.md +0 -105
- package/docs/schema-definition.md +0 -112
- package/metadata.json +0 -5
- package/playground/api/playground-api.ts +0 -94
- package/playground/index.html +0 -15
- package/playground/src/App.css +0 -1
- package/playground/src/App.tsx +0 -114
- package/playground/src/components/CodeDisplay.tsx +0 -43
- package/playground/src/components/QueryExecutor.tsx +0 -189
- package/playground/src/components/ResultsTable.tsx +0 -67
- package/playground/src/components/ResultsTabs.tsx +0 -105
- package/playground/src/components/ScenarioList.tsx +0 -56
- package/playground/src/components/logo.svg +0 -45
- package/playground/src/data/scenarios.ts +0 -2
- package/playground/src/main.tsx +0 -9
- package/playground/src/services/PlaygroundApiService.ts +0 -60
- package/postcss.config.cjs +0 -5
- package/sql_sql-ansi-cheatsheet-2025.md +0 -264
- package/src/ast/expression.ts +0 -658
- package/src/builder/operations/cte-manager.ts +0 -34
- package/src/builder/operations/filter-manager.ts +0 -68
- package/src/builder/operations/join-manager.ts +0 -36
- package/src/builder/operations/pagination-manager.ts +0 -36
- package/src/playground/features/playground/api/types.ts +0 -16
- package/src/playground/features/playground/clients/MockClient.ts +0 -17
- package/src/playground/features/playground/clients/SqliteClient.ts +0 -57
- package/src/playground/features/playground/common/IDatabaseClient.ts +0 -10
- package/src/playground/features/playground/data/scenarios/aggregation.ts +0 -36
- package/src/playground/features/playground/data/scenarios/basics.ts +0 -25
- package/src/playground/features/playground/data/scenarios/edge_cases.ts +0 -57
- package/src/playground/features/playground/data/scenarios/filtering.ts +0 -94
- package/src/playground/features/playground/data/scenarios/hydration.ts +0 -27
- package/src/playground/features/playground/data/scenarios/index.ts +0 -29
- package/src/playground/features/playground/data/scenarios/ordering.ts +0 -25
- package/src/playground/features/playground/data/scenarios/pagination.ts +0 -16
- package/src/playground/features/playground/data/scenarios/relationships.ts +0 -75
- package/src/playground/features/playground/data/scenarios/types.ts +0 -70
- package/src/playground/features/playground/data/schema.ts +0 -91
- package/src/playground/features/playground/data/seed.ts +0 -104
- package/src/playground/features/playground/services/QueryExecutionService.ts +0 -121
- package/src/runtime/orm-context.ts +0 -539
- package/tests/belongs-to-many.test.ts +0 -57
- package/tests/between.test.ts +0 -43
- package/tests/case-expression.test.ts +0 -58
- package/tests/complex-exists.test.ts +0 -230
- package/tests/cte.test.ts +0 -118
- package/tests/dml.test.ts +0 -206
- package/tests/exists.test.ts +0 -127
- package/tests/like.test.ts +0 -33
- package/tests/orm-runtime.test.ts +0 -254
- package/tests/postgres.test.ts +0 -30
- package/tests/right-join.test.ts +0 -89
- package/tests/subquery-having.test.ts +0 -193
- package/tests/window-function.test.ts +0 -151
- package/tsconfig.json +0 -30
- package/tsup.config.ts +0 -10
- package/vite.config.ts +0 -22
- package/vitest.config.ts +0 -14
- /package/src/{constants → core/sql}/sql.ts +0 -0
- /package/src/{runtime → orm}/als.ts +0 -0
- /package/src/{utils → query-builder}/relation-alias.ts +0 -0
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { ColumnDef } from '../../schema/column';
|
|
2
|
-
import { ColumnNode, ExpressionNode } from '../../ast/expression';
|
|
3
|
-
import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps';
|
|
4
|
-
import { OrderDirection } from '../../constants/sql';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Manages filtering, ordering, and grouping operations for queries
|
|
8
|
-
*/
|
|
9
|
-
export class FilterManager {
|
|
10
|
-
/**
|
|
11
|
-
* Creates a new FilterManager instance
|
|
12
|
-
* @param env - Query builder environment
|
|
13
|
-
*/
|
|
14
|
-
constructor(private readonly env: SelectQueryBuilderEnvironment) {}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Adds a WHERE clause to the query
|
|
18
|
-
* @param context - Current query context
|
|
19
|
-
* @param expr - Expression for the WHERE clause
|
|
20
|
-
* @returns Updated query context with WHERE clause
|
|
21
|
-
*/
|
|
22
|
-
where(context: SelectQueryBuilderContext, expr: ExpressionNode): SelectQueryBuilderContext {
|
|
23
|
-
const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
|
|
24
|
-
const nextState = astService.withWhere(expr);
|
|
25
|
-
return { state: nextState, hydration: context.hydration };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Adds a GROUP BY clause to the query
|
|
30
|
-
* @param context - Current query context
|
|
31
|
-
* @param column - Column to group by
|
|
32
|
-
* @returns Updated query context with GROUP BY clause
|
|
33
|
-
*/
|
|
34
|
-
groupBy(context: SelectQueryBuilderContext, column: ColumnDef | ColumnNode): SelectQueryBuilderContext {
|
|
35
|
-
const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
|
|
36
|
-
const nextState = astService.withGroupBy(column);
|
|
37
|
-
return { state: nextState, hydration: context.hydration };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Adds a HAVING clause to the query
|
|
42
|
-
* @param context - Current query context
|
|
43
|
-
* @param expr - Expression for the HAVING clause
|
|
44
|
-
* @returns Updated query context with HAVING clause
|
|
45
|
-
*/
|
|
46
|
-
having(context: SelectQueryBuilderContext, expr: ExpressionNode): SelectQueryBuilderContext {
|
|
47
|
-
const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
|
|
48
|
-
const nextState = astService.withHaving(expr);
|
|
49
|
-
return { state: nextState, hydration: context.hydration };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Adds an ORDER BY clause to the query
|
|
54
|
-
* @param context - Current query context
|
|
55
|
-
* @param column - Column to order by
|
|
56
|
-
* @param direction - Order direction (ASC/DESC)
|
|
57
|
-
* @returns Updated query context with ORDER BY clause
|
|
58
|
-
*/
|
|
59
|
-
orderBy(
|
|
60
|
-
context: SelectQueryBuilderContext,
|
|
61
|
-
column: ColumnDef | ColumnNode,
|
|
62
|
-
direction: OrderDirection
|
|
63
|
-
): SelectQueryBuilderContext {
|
|
64
|
-
const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
|
|
65
|
-
const nextState = astService.withOrderBy(column, direction);
|
|
66
|
-
return { state: nextState, hydration: context.hydration };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { BinaryExpressionNode } from '../../ast/expression';
|
|
2
|
-
import { TableDef } from '../../schema/table';
|
|
3
|
-
import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps';
|
|
4
|
-
import { JoinKind } from '../../constants/sql';
|
|
5
|
-
import { createJoinNode } from '../../utils/join-node';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Manages JOIN operations for query building
|
|
9
|
-
*/
|
|
10
|
-
export class JoinManager {
|
|
11
|
-
/**
|
|
12
|
-
* Creates a new JoinManager instance
|
|
13
|
-
* @param env - Query builder environment
|
|
14
|
-
*/
|
|
15
|
-
constructor(private readonly env: SelectQueryBuilderEnvironment) {}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Adds a JOIN clause to the query
|
|
19
|
-
* @param context - Current query context
|
|
20
|
-
* @param table - Table to join
|
|
21
|
-
* @param condition - Join condition expression
|
|
22
|
-
* @param kind - Type of join (INNER, LEFT, RIGHT, etc.)
|
|
23
|
-
* @returns Updated query context with JOIN clause
|
|
24
|
-
*/
|
|
25
|
-
join(
|
|
26
|
-
context: SelectQueryBuilderContext,
|
|
27
|
-
table: TableDef,
|
|
28
|
-
condition: BinaryExpressionNode,
|
|
29
|
-
kind: JoinKind
|
|
30
|
-
): SelectQueryBuilderContext {
|
|
31
|
-
const joinNode = createJoinNode(kind, table.name, condition);
|
|
32
|
-
const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
|
|
33
|
-
const nextState = astService.withJoin(joinNode);
|
|
34
|
-
return { state: nextState, hydration: context.hydration };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Manages pagination operations (LIMIT and OFFSET) for queries
|
|
5
|
-
*/
|
|
6
|
-
export class PaginationManager {
|
|
7
|
-
/**
|
|
8
|
-
* Creates a new PaginationManager instance
|
|
9
|
-
* @param env - Query builder environment
|
|
10
|
-
*/
|
|
11
|
-
constructor(private readonly env: SelectQueryBuilderEnvironment) {}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Adds a LIMIT clause to the query
|
|
15
|
-
* @param context - Current query context
|
|
16
|
-
* @param value - Maximum number of rows to return
|
|
17
|
-
* @returns Updated query context with LIMIT clause
|
|
18
|
-
*/
|
|
19
|
-
limit(context: SelectQueryBuilderContext, value: number): SelectQueryBuilderContext {
|
|
20
|
-
const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
|
|
21
|
-
const nextState = astService.withLimit(value);
|
|
22
|
-
return { state: nextState, hydration: context.hydration };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Adds an OFFSET clause to the query
|
|
27
|
-
* @param context - Current query context
|
|
28
|
-
* @param value - Number of rows to skip
|
|
29
|
-
* @returns Updated query context with OFFSET clause
|
|
30
|
-
*/
|
|
31
|
-
offset(context: SelectQueryBuilderContext, value: number): SelectQueryBuilderContext {
|
|
32
|
-
const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
|
|
33
|
-
const nextState = astService.withOffset(value);
|
|
34
|
-
return { state: nextState, hydration: context.hydration };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { QueryResult } from '../common/IDatabaseClient';
|
|
2
|
-
|
|
3
|
-
export interface QueryExecutionResult {
|
|
4
|
-
sql: string;
|
|
5
|
-
params: unknown[];
|
|
6
|
-
typescriptCode: string;
|
|
7
|
-
results: QueryResult[];
|
|
8
|
-
hydratedResults?: Record<string, any>[];
|
|
9
|
-
error: string | null;
|
|
10
|
-
executionTime: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ApiStatusResponse {
|
|
14
|
-
ready: boolean;
|
|
15
|
-
error: string | null;
|
|
16
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { IDatabaseClient, QueryResult } from "../common/IDatabaseClient";
|
|
2
|
-
import { DialectName } from "../../../../constants/sql";
|
|
3
|
-
|
|
4
|
-
export type SupportedDialect = DialectName;
|
|
5
|
-
|
|
6
|
-
export class MockClient implements IDatabaseClient {
|
|
7
|
-
isReady: boolean = true;
|
|
8
|
-
error: string | null = null;
|
|
9
|
-
|
|
10
|
-
constructor(dialect: SupportedDialect) {
|
|
11
|
-
this.error = `${dialect} is not supported yet.`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
public async executeSql(sql: string, _params: unknown[] = []): Promise<QueryResult[]> {
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { IDatabaseClient, QueryResult } from "../common/IDatabaseClient";
|
|
2
|
-
import { SEED_SQL } from "../data/seed";
|
|
3
|
-
import sqlite3 from 'sqlite3';
|
|
4
|
-
|
|
5
|
-
export class SqliteClient implements IDatabaseClient {
|
|
6
|
-
isReady: boolean = false;
|
|
7
|
-
error: string | null = null;
|
|
8
|
-
private db: sqlite3.Database | null = null;
|
|
9
|
-
|
|
10
|
-
constructor() {
|
|
11
|
-
this.initDB();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
private async initDB() {
|
|
15
|
-
try {
|
|
16
|
-
this.db = new sqlite3.Database(':memory:');
|
|
17
|
-
await new Promise<void>((resolve, reject) => {
|
|
18
|
-
this.db!.exec(SEED_SQL, (err) => {
|
|
19
|
-
if (err) reject(err);
|
|
20
|
-
else resolve();
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
this.isReady = true;
|
|
24
|
-
} catch (e) {
|
|
25
|
-
console.error("Failed to load DB", e);
|
|
26
|
-
this.error = "Failed to initialize SQLite database.";
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
public async executeSql(sql: string, params: unknown[] = []): Promise<QueryResult[]> {
|
|
31
|
-
if (!this.db) {
|
|
32
|
-
this.error = "Database not ready.";
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return new Promise((resolve, reject) => {
|
|
37
|
-
this.db!.all(sql, params, (err, rows) => {
|
|
38
|
-
if (err) {
|
|
39
|
-
this.error = err.message;
|
|
40
|
-
resolve([]);
|
|
41
|
-
} else {
|
|
42
|
-
this.error = null;
|
|
43
|
-
if (rows.length === 0) {
|
|
44
|
-
resolve([]);
|
|
45
|
-
} else {
|
|
46
|
-
const columns = Object.keys(rows[0]);
|
|
47
|
-
const values = rows.map(row => columns.map(col => row[col]));
|
|
48
|
-
resolve([{
|
|
49
|
-
columns,
|
|
50
|
-
values,
|
|
51
|
-
}]);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { eq, count, sum } from '../../../../../ast/expression';
|
|
2
|
-
import { createLiteral } from '../../../../../builder/select';
|
|
3
|
-
import { Users, Orders } from '../schema';
|
|
4
|
-
import { Scenario } from './types';
|
|
5
|
-
|
|
6
|
-
export const AGGREGATION_SCENARIOS = [
|
|
7
|
-
{
|
|
8
|
-
id: 'analytics',
|
|
9
|
-
category: 'Aggregation',
|
|
10
|
-
title: 'Sales Analytics',
|
|
11
|
-
description: 'Aggregating sales data using GROUP BY and sorting with ORDER BY DESC.',
|
|
12
|
-
build: (qb) => qb
|
|
13
|
-
.select({
|
|
14
|
-
user: Users.columns.name,
|
|
15
|
-
orderCount: count(Orders.columns.id)
|
|
16
|
-
})
|
|
17
|
-
.joinRelation('orders')
|
|
18
|
-
.groupBy(Users.columns.name)
|
|
19
|
-
.orderBy(Users.columns.name, 'DESC')
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
id: 'revenue_by_role',
|
|
23
|
-
category: 'Aggregation',
|
|
24
|
-
title: 'Revenue by Role',
|
|
25
|
-
description: 'Summing completed order totals and grouping the result by user role.',
|
|
26
|
-
build: (qb) => qb
|
|
27
|
-
.select({
|
|
28
|
-
role: Users.columns.role,
|
|
29
|
-
revenue: sum(Orders.columns.total)
|
|
30
|
-
})
|
|
31
|
-
.joinRelation('orders')
|
|
32
|
-
.where(eq(Orders.columns.status, createLiteral('completed')))
|
|
33
|
-
.groupBy(Users.columns.role)
|
|
34
|
-
.orderBy(Users.columns.role, 'ASC')
|
|
35
|
-
}
|
|
36
|
-
];
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Users } from '../schema';
|
|
2
|
-
import { createScenario } from './types';
|
|
3
|
-
|
|
4
|
-
export const BASIC_SCENARIOS = [
|
|
5
|
-
createScenario({
|
|
6
|
-
id: 'basic',
|
|
7
|
-
category: 'Basics',
|
|
8
|
-
title: 'Hello World',
|
|
9
|
-
description: 'A basic projection query fetching specific columns from the Users table.',
|
|
10
|
-
build: (qb) => qb.select({ id: Users.columns.id, name: Users.columns.name }).limit(5)
|
|
11
|
-
}),
|
|
12
|
-
createScenario({
|
|
13
|
-
id: 'aliased_projection',
|
|
14
|
-
category: 'Basics',
|
|
15
|
-
title: 'Aliased Projection',
|
|
16
|
-
description: 'Expose friendly aliases when projecting columns for DTOs.',
|
|
17
|
-
build: (qb) => qb
|
|
18
|
-
.select({
|
|
19
|
-
userId: Users.columns.id,
|
|
20
|
-
userName: Users.columns.name,
|
|
21
|
-
userRole: Users.columns.role
|
|
22
|
-
})
|
|
23
|
-
.limit(4)
|
|
24
|
-
})
|
|
25
|
-
];
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { eq, and, or, inList, jsonPath, isNull } from '../../../../../ast/expression';
|
|
2
|
-
import { createLiteral } from '../../../../../builder/select';
|
|
3
|
-
import { Users } from '../schema';
|
|
4
|
-
import { Scenario } from './types';
|
|
5
|
-
|
|
6
|
-
export const EDGE_CASE_SCENARIOS = [
|
|
7
|
-
{
|
|
8
|
-
id: 'empty_in_list',
|
|
9
|
-
category: 'Edge Cases',
|
|
10
|
-
title: 'Empty IN List',
|
|
11
|
-
description: 'Filtering with an empty list in IN clause. Should return no results or handle gracefully.',
|
|
12
|
-
build: (qb) => qb
|
|
13
|
-
.select({ name: Users.columns.name })
|
|
14
|
-
.where(inList(Users.columns.id, []))
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
id: 'deep_nested_logic',
|
|
18
|
-
category: 'Edge Cases',
|
|
19
|
-
title: 'Deeply Nested Logic',
|
|
20
|
-
description: 'Complex nested AND/OR logic to test parser/compiler robustness.',
|
|
21
|
-
build: (qb) => qb
|
|
22
|
-
.select({ name: Users.columns.name })
|
|
23
|
-
.where(
|
|
24
|
-
or(
|
|
25
|
-
and(
|
|
26
|
-
eq(Users.columns.role, 'admin'),
|
|
27
|
-
or(
|
|
28
|
-
eq(Users.columns.name, 'Alice'),
|
|
29
|
-
eq(Users.columns.name, 'Bob')
|
|
30
|
-
)
|
|
31
|
-
),
|
|
32
|
-
and(
|
|
33
|
-
eq(Users.columns.role, 'user'),
|
|
34
|
-
isNull(Users.columns.deleted_at)
|
|
35
|
-
)
|
|
36
|
-
)
|
|
37
|
-
)
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
id: 'json_path_missing',
|
|
41
|
-
category: 'Edge Cases',
|
|
42
|
-
title: 'Non-existent JSON Path',
|
|
43
|
-
description: 'Querying a JSON path that likely does not exist.',
|
|
44
|
-
build: (qb) => qb
|
|
45
|
-
.select({ name: Users.columns.name })
|
|
46
|
-
.where(eq(jsonPath(Users.columns.settings, '$.non_existent_key'), createLiteral('some_value')))
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
id: 'zero_limit',
|
|
50
|
-
category: 'Edge Cases',
|
|
51
|
-
title: 'Zero Limit',
|
|
52
|
-
description: 'Limit 0 should return empty result set.',
|
|
53
|
-
build: (qb) => qb
|
|
54
|
-
.select({ name: Users.columns.name })
|
|
55
|
-
.limit(0)
|
|
56
|
-
}
|
|
57
|
-
];
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { eq, and, or, gt, like, jsonPath, isNull, isNotNull, inList } from '../../../../../ast/expression';
|
|
2
|
-
import { createLiteral } from '../../../../../builder/select';
|
|
3
|
-
import { Users, Orders } from '../schema';
|
|
4
|
-
import { createScenario } from './types';
|
|
5
|
-
|
|
6
|
-
export const FILTERING_SCENARIOS = [
|
|
7
|
-
createScenario({
|
|
8
|
-
id: 'filter_basic',
|
|
9
|
-
category: 'Filtering',
|
|
10
|
-
title: 'Active Admins',
|
|
11
|
-
description: 'Filtering records using a WHERE clause with type-safe operators.',
|
|
12
|
-
build: (qb) => qb
|
|
13
|
-
.select({ name: Users.columns.name, role: Users.columns.role })
|
|
14
|
-
.where(eq(Users.columns.role, createLiteral('admin')))
|
|
15
|
-
}),
|
|
16
|
-
createScenario({
|
|
17
|
-
id: 'search',
|
|
18
|
-
category: 'Filtering',
|
|
19
|
-
title: 'Text Search (LIKE)',
|
|
20
|
-
description: 'Using pattern matching to find users starting with "Alice".',
|
|
21
|
-
build: (qb) => qb
|
|
22
|
-
.select({ name: Users.columns.name, role: Users.columns.role })
|
|
23
|
-
.where(like(Users.columns.name, 'Alice%'))
|
|
24
|
-
}),
|
|
25
|
-
createScenario({
|
|
26
|
-
id: 'json_filter',
|
|
27
|
-
category: 'Filtering',
|
|
28
|
-
title: 'JSON Extract',
|
|
29
|
-
description: 'Extracting values from JSON columns using dialect-specific syntax.',
|
|
30
|
-
build: (qb) => qb
|
|
31
|
-
.select({ name: Users.columns.name, settings: Users.columns.settings })
|
|
32
|
-
.where(eq(jsonPath(Users.columns.settings, '$.theme'), createLiteral('dark')))
|
|
33
|
-
}),
|
|
34
|
-
createScenario({
|
|
35
|
-
id: 'advanced_filter',
|
|
36
|
-
category: 'Filtering',
|
|
37
|
-
title: 'Nulls & Lists',
|
|
38
|
-
description: 'Advanced filtering using IN(...) clause and IS NULL checks.',
|
|
39
|
-
build: (qb) => qb
|
|
40
|
-
.select({ name: Users.columns.name, role: Users.columns.role })
|
|
41
|
-
.where(
|
|
42
|
-
and(
|
|
43
|
-
inList(Users.columns.role, ['admin', 'manager']),
|
|
44
|
-
isNull(Users.columns.deleted_at)
|
|
45
|
-
)
|
|
46
|
-
)
|
|
47
|
-
}),
|
|
48
|
-
createScenario({
|
|
49
|
-
id: 'complex_filter',
|
|
50
|
-
category: 'Filtering',
|
|
51
|
-
title: 'Logical Groups',
|
|
52
|
-
description: 'Using Nested AND/OR to filter for Admins OR users with specific ID.',
|
|
53
|
-
build: (qb) => qb
|
|
54
|
-
.select({ name: Users.columns.name, role: Users.columns.role })
|
|
55
|
-
.where(
|
|
56
|
-
or(
|
|
57
|
-
eq(Users.columns.role, createLiteral('admin')),
|
|
58
|
-
and(
|
|
59
|
-
eq(Users.columns.role, createLiteral('user')),
|
|
60
|
-
eq(Users.columns.id, createLiteral(2))
|
|
61
|
-
)
|
|
62
|
-
)
|
|
63
|
-
)
|
|
64
|
-
}),
|
|
65
|
-
createScenario({
|
|
66
|
-
id: 'soft_deleted_users',
|
|
67
|
-
category: 'Filtering',
|
|
68
|
-
title: 'Soft Deleted Users',
|
|
69
|
-
description: 'Reveal users that were soft-deleted via a non-null deleted_at.',
|
|
70
|
-
build: (qb) => qb
|
|
71
|
-
.select({ name: Users.columns.name, deletedAt: Users.columns.deleted_at })
|
|
72
|
-
.where(isNotNull(Users.columns.deleted_at))
|
|
73
|
-
}),
|
|
74
|
-
createScenario({
|
|
75
|
-
id: 'high_value_admins',
|
|
76
|
-
category: 'Filtering',
|
|
77
|
-
title: 'High Value Admins',
|
|
78
|
-
description: 'Filter admins with completed orders exceeding 200 via a joined WHERE clause.',
|
|
79
|
-
build: (qb) => qb
|
|
80
|
-
.select({
|
|
81
|
-
name: Users.columns.name,
|
|
82
|
-
role: Users.columns.role,
|
|
83
|
-
total: Orders.columns.total
|
|
84
|
-
})
|
|
85
|
-
.joinRelation('orders')
|
|
86
|
-
.where(
|
|
87
|
-
and(
|
|
88
|
-
eq(Users.columns.role, createLiteral('admin')),
|
|
89
|
-
gt(Orders.columns.total, createLiteral(200)),
|
|
90
|
-
eq(Orders.columns.status, createLiteral('completed'))
|
|
91
|
-
)
|
|
92
|
-
)
|
|
93
|
-
})
|
|
94
|
-
];
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { eq } from '../../../../../ast/expression';
|
|
2
|
-
import { Users } from '../schema';
|
|
3
|
-
import { createScenario } from './types';
|
|
4
|
-
|
|
5
|
-
export const HYDRATION_SCENARIOS = [
|
|
6
|
-
createScenario({
|
|
7
|
-
id: 'user_with_orders',
|
|
8
|
-
category: 'Hydration',
|
|
9
|
-
title: 'Nested User Graph',
|
|
10
|
-
description: 'Eager-load orders for a single user and hydrate a JSON graph from flat SQL rows.',
|
|
11
|
-
build: (qb) => qb
|
|
12
|
-
.include('orders', { columns: ['id', 'total', 'status', 'user_id'] })
|
|
13
|
-
.where(eq(Users.columns.id, 1))
|
|
14
|
-
}),
|
|
15
|
-
createScenario({
|
|
16
|
-
id: 'user_projects_with_pivot',
|
|
17
|
-
category: 'Hydration',
|
|
18
|
-
title: 'Projects with Pivot Data',
|
|
19
|
-
description: 'Include projects for a user along with pivot metadata stored in `_pivot`.',
|
|
20
|
-
build: (qb) => qb
|
|
21
|
-
.include('projects', {
|
|
22
|
-
columns: ['id', 'name', 'client'],
|
|
23
|
-
pivot: { columns: ['assigned_at', 'role_id'] }
|
|
24
|
-
})
|
|
25
|
-
.where(eq(Users.columns.id, 1))
|
|
26
|
-
})
|
|
27
|
-
];
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { Scenario } from './types';
|
|
2
|
-
import { BASIC_SCENARIOS } from './basics';
|
|
3
|
-
import { FILTERING_SCENARIOS } from './filtering';
|
|
4
|
-
import { RELATIONSHIP_SCENARIOS } from './relationships';
|
|
5
|
-
import { AGGREGATION_SCENARIOS } from './aggregation';
|
|
6
|
-
import { PAGINATION_SCENARIOS } from './pagination';
|
|
7
|
-
import { ORDERING_SCENARIOS } from './ordering';
|
|
8
|
-
import { HYDRATION_SCENARIOS } from './hydration';
|
|
9
|
-
import { EDGE_CASE_SCENARIOS } from './edge_cases';
|
|
10
|
-
|
|
11
|
-
export * from './types';
|
|
12
|
-
export { BASIC_SCENARIOS };
|
|
13
|
-
export { FILTERING_SCENARIOS };
|
|
14
|
-
export { RELATIONSHIP_SCENARIOS };
|
|
15
|
-
export { AGGREGATION_SCENARIOS };
|
|
16
|
-
export { PAGINATION_SCENARIOS };
|
|
17
|
-
export { ORDERING_SCENARIOS };
|
|
18
|
-
export { HYDRATION_SCENARIOS };
|
|
19
|
-
|
|
20
|
-
export const SCENARIOS: Scenario[] = [
|
|
21
|
-
...BASIC_SCENARIOS,
|
|
22
|
-
...FILTERING_SCENARIOS,
|
|
23
|
-
...RELATIONSHIP_SCENARIOS,
|
|
24
|
-
...AGGREGATION_SCENARIOS,
|
|
25
|
-
...ORDERING_SCENARIOS,
|
|
26
|
-
...PAGINATION_SCENARIOS,
|
|
27
|
-
...HYDRATION_SCENARIOS,
|
|
28
|
-
...EDGE_CASE_SCENARIOS
|
|
29
|
-
];
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { eq } from '../../../../../ast/expression';
|
|
3
|
-
import { createLiteral } from '../../../../../builder/select';
|
|
4
|
-
import { Users, Orders } from '../schema';
|
|
5
|
-
import { Scenario } from './types';
|
|
6
|
-
|
|
7
|
-
export const ORDERING_SCENARIOS: Scenario[] = [
|
|
8
|
-
{
|
|
9
|
-
id: 'order_recent_completed',
|
|
10
|
-
category: 'Ordering',
|
|
11
|
-
title: 'Order by Completed Total',
|
|
12
|
-
description: 'Sort completed orders first by total (DESC) then by user name (ASC).',
|
|
13
|
-
build: (qb) => qb
|
|
14
|
-
.select({
|
|
15
|
-
user: Users.columns.name,
|
|
16
|
-
status: Orders.columns.status,
|
|
17
|
-
total: Orders.columns.total
|
|
18
|
-
})
|
|
19
|
-
.joinRelation('orders')
|
|
20
|
-
.where(eq(Orders.columns.status, createLiteral('completed')))
|
|
21
|
-
.orderBy(Orders.columns.total, 'DESC')
|
|
22
|
-
.orderBy(Users.columns.name, 'ASC')
|
|
23
|
-
.limit(5)
|
|
24
|
-
}
|
|
25
|
-
];
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Users } from '../schema';
|
|
2
|
-
import { Scenario } from './types';
|
|
3
|
-
|
|
4
|
-
export const PAGINATION_SCENARIOS: Scenario[] = [
|
|
5
|
-
{
|
|
6
|
-
id: 'pagination',
|
|
7
|
-
category: 'Pagination',
|
|
8
|
-
title: 'Basic Pagination',
|
|
9
|
-
description: 'Implement basic pagination with LIMIT and OFFSET.',
|
|
10
|
-
build: (qb) => qb
|
|
11
|
-
.select({ name: Users.columns.name, role: Users.columns.role })
|
|
12
|
-
.orderBy(Users.columns.name, 'ASC')
|
|
13
|
-
.limit(2)
|
|
14
|
-
.offset(1)
|
|
15
|
-
}
|
|
16
|
-
];
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { eq, gt, like, inList } from '../../../../../ast/expression';
|
|
2
|
-
import { Users, Orders, Profiles, Roles, UserRoles } from '../schema';
|
|
3
|
-
import { Scenario } from './types';
|
|
4
|
-
|
|
5
|
-
export const RELATIONSHIP_SCENARIOS: Scenario[] = [
|
|
6
|
-
{
|
|
7
|
-
id: 'smart_join',
|
|
8
|
-
category: 'Relationships',
|
|
9
|
-
title: 'Smart Join (1:N)',
|
|
10
|
-
description: 'Using defined schema relationships to join tables automatically without manual ON clauses.',
|
|
11
|
-
build: (qb) => qb
|
|
12
|
-
.select({ user: Users.columns.name, total: Orders.columns.total })
|
|
13
|
-
.joinRelation('orders')
|
|
14
|
-
.where(gt(Orders.columns.total, 100))
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
id: 'join',
|
|
18
|
-
category: 'Relationships',
|
|
19
|
-
title: 'Manual Join',
|
|
20
|
-
description: 'Performing a manual INNER JOIN with explicit conditions.',
|
|
21
|
-
build: (qb) => qb
|
|
22
|
-
.select({ user: Users.columns.name, amount: Orders.columns.total, status: Orders.columns.status })
|
|
23
|
-
.innerJoin(Orders, eq(Users.columns.id, Orders.columns.user_id))
|
|
24
|
-
.where(eq(Orders.columns.status, 'completed'))
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
id: 'profile_inspection',
|
|
28
|
-
category: 'Relationships',
|
|
29
|
-
title: 'Profile Inspection (1:1)',
|
|
30
|
-
description: 'Join every user with their profile and filter by bio keywords.',
|
|
31
|
-
build: (qb) => qb
|
|
32
|
-
.select({
|
|
33
|
-
user: Users.columns.name,
|
|
34
|
-
bio: Profiles.columns.bio,
|
|
35
|
-
twitter: Profiles.columns.twitter
|
|
36
|
-
})
|
|
37
|
-
.joinRelation('profiles')
|
|
38
|
-
.where(like(Profiles.columns.bio, '%Engineer%'))
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
id: 'role_filter',
|
|
42
|
-
category: 'Relationships',
|
|
43
|
-
title: 'Role-Based Filter (N:N)',
|
|
44
|
-
description: 'Traverse the pivot table to surface admins and managers.',
|
|
45
|
-
build: (qb) => qb
|
|
46
|
-
.select({
|
|
47
|
-
user: Users.columns.name,
|
|
48
|
-
role: Roles.columns.name
|
|
49
|
-
})
|
|
50
|
-
.innerJoin(UserRoles, eq(UserRoles.columns.user_id, Users.columns.id))
|
|
51
|
-
.innerJoin(Roles, eq(Roles.columns.id, UserRoles.columns.role_id))
|
|
52
|
-
.where(inList(Roles.columns.name, ['admin', 'manager']))
|
|
53
|
-
.orderBy(Users.columns.name, 'ASC')
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: 'contain_orders',
|
|
57
|
-
category: 'Relationships',
|
|
58
|
-
title: 'Contain Orders (LEFT JOIN + filter)',
|
|
59
|
-
description: 'Fetch users and hydrate only completed orders without dropping users that have none.',
|
|
60
|
-
build: (qb) => qb
|
|
61
|
-
.include('orders', { columns: ['id', 'total', 'status', 'user_id'], filter: eq(Orders.columns.status, 'completed') })
|
|
62
|
-
.select({ id: Users.columns.id, name: Users.columns.name, role: Users.columns.role })
|
|
63
|
-
.orderBy(Users.columns.id, 'ASC')
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
id: 'match_orders',
|
|
67
|
-
category: 'Relationships',
|
|
68
|
-
title: 'Match Orders (INNER JOIN + DISTINCT)',
|
|
69
|
-
description: 'Return only users that match a relation predicate, similar to CakePHP matching().',
|
|
70
|
-
build: (qb) => qb
|
|
71
|
-
.match('orders', eq(Orders.columns.status, 'completed'))
|
|
72
|
-
.select({ id: Users.columns.id, name: Users.columns.name })
|
|
73
|
-
.orderBy(Users.columns.id, 'ASC')
|
|
74
|
-
}
|
|
75
|
-
];
|