Skip to content

Commit 9f24acc

Browse files
committed
test: wip for functional final tests
1 parent bd66cd4 commit 9f24acc

72 files changed

Lines changed: 7713 additions & 455 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# @devscast/datazen
22

33
# Unreleased
4+
- Functional/Statement parity pass (Datazen-reference): replaced blanket skips in `src/__tests__/functional/statement.test.ts` with runtime-gated behavior aligned to Datazen intent. Added `binds invalid named parameter` (runs only on named-binding drivers), added `fetches long BLOB values` using Node `convertToNodeValue` semantics, and replaced strict numeric assertions with normalized numeric comparisons to match cross-driver equality intent. Also gated `executes with redundant parameters` for drivers/platforms that do not report redundant bindings (named-binding + MySQL-family), and validated this file on `sqlite3`, `mysql`, `mariadb`, `postgresql`, and `sqlserver`.
5+
- Schema parity pass (Datazen-reference): aligned `AbstractSchemaManager` list APIs with legacy schema-fetch behavior (`listTableColumns`, `listTableIndexes`, `listTableForeignKeys`) and fixed `introspectTablePrimaryKeyConstraint()` to return `null` when a table is missing. Also aligned FK/PK metadata processors to quoted-name semantics (`PrimaryKeyConstraintColumnMetadataProcessor`, `ForeignKeyConstraintColumnMetadataProcessor`), normalized comparator platform-option null-vs-undefined comparison noise, and updated affected parity tests (`schema-manager`, `foreign-key-constraint-editor`, `abstract-comparator-test-case`, `metadata-providers`, `platform-parity`, `schema-name-introspection`) to match Datazen intent.
6+
- Functional/Schema vendor-matrix pass: replaced remaining placeholder `it.skip(...)` files with real, runtime-gated Datazen-reference tests in `src/__tests__/functional/schema/{sqlite-schema-manager,mysql-schema-manager,sql-server-schema-manager,db2-schema-manager,oracle-schema-manager}.test.ts` plus `src/__tests__/functional/schema/mysql/json-collation.test.ts` and `src/__tests__/functional/schema/oracle/comparator.test.ts`.
7+
- Schema manager parity: aligned unsupported database-name introspection behavior with Datazen expectations for SQLite/DB2 by making `listDatabases()` explicitly throw `NotSupported` in `src/schema/sqlite-schema-manager.ts` and `src/schema/db2-schema-manager.ts` (instead of returning empty lists).
8+
- Functional/Schema SQLite comparator parity: replaced `src/__tests__/functional/schema/sqlite/comparator.test.ts` placeholder with a Datazen-aligned `ComparatorTest::testChangeTableCollation` port (real diff/apply/re-diff flow), and fixed the underlying SQLite implementation gaps by enabling SQLite column-collation support in `src/platforms/sqlite-platform.ts`, wiring `SQLiteSchemaManager.createComparator()` to the SQLite-specific comparator in `src/schema/sqlite-schema-manager.ts`, preserving renamed-column state when cloning in `src/platforms/sqlite/comparator.ts`, and extending SQLite metadata introspection in `src/platforms/sqlite/sqlite-metadata-provider.ts` to parse column collations from table DDL (with `BINARY` default normalization compatibility).
9+
- Functional/Schema parity: replaced the `Functional/Schema/AlterTableTest` placeholder with a Doctrine-aligned port in `src/__tests__/functional/schema/alter-table.test.ts` (primary-key migration matrix, FK replacement migration, and Doctrine-equivalent platform/runtime skips).
10+
- SQLite alter-table parity: aligned `SQLitePlatform.getAlterTableSQL()` with Doctrine behavior by adding a real "simple add-column" gate and routing all non-simple index/foreign-key/PK alter cases through table-rebuild SQL generation (instead of generic `ALTER TABLE`/`DROP INDEX` paths that SQLite cannot execute for these diffs).
11+
- Foreign-key comparator parity: aligned `ForeignKeyConstraint.onUpdate()/onDelete()` normalization with Doctrine semantics in `src/schema/foreign-key-constraint.ts` (`NO ACTION` and `RESTRICT` now normalize to `null` for diff comparison), which removes false-positive FK diffs after SQLite introspection/rebuild.
12+
- Functional/Schema parity pass: replaced placeholder skips with Doctrine-aligned ports for `Functional/Schema/ForeignKeyConstraintTest`, `Functional/Schema/CustomIntrospectionTest`, and `Functional/Schema/SchemaManagerTest` in `src/__tests__/functional/schema/{foreign-key-constraint,custom-introspection,schema-manager}.test.ts` (with platform/runtime skips only where Doctrine intent requires capability gating).
13+
- Schema/runtime parity fixes surfaced by the new ports: fixed unnamed foreign-key storage in `src/schema/table.ts` so multiple unnamed FKs are preserved (no empty-name overwrite), made schema-manager table introspection lookup on non-schema platforms tolerant of quoted/dotted/legacy-invalid identifier forms in `src/schema/abstract-schema-manager.ts`, and improved SQLite metadata introspection in `src/platforms/sqlite/sqlite-metadata-provider.ts` by inferring identity/autoincrement for single integer primary-key columns (matching Doctrine SQLite behavior expectations).
414
- Local functional runner: added `bun run test:functional:local` (backed by `scripts/run-functional-tests-local.mjs`) to run the full functional suite sequentially against local Docker-backed `sqlite3`, `mysql`, `mariadb`, `postgresql`, and `sqlserver` targets. The runner reuses the same JSON profiles in `ci/github/vitest/*.json` and applies the local compose MariaDB port override (`3307`) automatically.
515
- MySQL comparator metadata refactor (async I/O parity): `CharsetMetadataProvider` and `CollationMetadataProvider` are now async (`Promise<string | null>`), connection-backed and caching providers were converted to async/await, and the legacy sync DB query contract (`src/platforms/_internal/sync-query-connection.ts`) was removed. MySQL schema manager comparator wiring now preloads charset/collation metadata asynchronously in `initialize()` and constructs the MySQL comparator with in-memory lookup maps so `compareTables()` stays pure/synchronous while all DB access is awaited ahead of time. Also preserved explicit `renamedColumns` state when cloning tables inside the MySQL comparator and restored Doctrine comparator defaults (`ComparatorConfig` rename/index detection and modified-index reporting default to `true`).
616
- Functional/Platform parity (alter/rename batch): added Doctrine-based ports for `Functional/Platform/AlterColumnLengthChangeTest`, `AlterDecimalColumnTest`, `AlterColumnTest`, and `RenameColumnTest` in `src/__tests__/functional/platform/*.test.ts`, and validated the batch on real `sqlite3`, `mysql`, `mariadb`, `postgresql`, and `sqlserver` targets (with only Doctrine-intent runtime skips preserved in `AlterColumnTest` where applicable).

scripts/doctrine-test-parity-report.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,28 @@ const portedFunctionalFiles = new Set([
6262
"Functional/Types/JsonObjectTest.php",
6363
"Functional/Types/JsonTest.php",
6464
"Functional/Types/NumberTest.php",
65+
"Functional/Query/QueryBuilderTest.php",
66+
"Functional/SQL/Builder/CreateAndDropSchemaObjectsSQLBuilderTest.php",
67+
"Functional/Schema/ColumnCommentTest.php",
68+
"Functional/Schema/ColumnRenameTest.php",
69+
"Functional/Schema/ComparatorTest.php",
70+
"Functional/Schema/UniqueConstraintTest.php",
71+
"Functional/Schema/AlterTableTest.php",
72+
"Functional/Schema/CustomIntrospectionTest.php",
73+
"Functional/Schema/Db2SchemaManagerTest.php",
74+
"Functional/Schema/ForeignKeyConstraintTest.php",
75+
"Functional/Schema/MySQL/ComparatorTest.php",
76+
"Functional/Schema/MySQL/JsonCollationTest.php",
77+
"Functional/Schema/MySQLSchemaManagerTest.php",
78+
"Functional/Schema/Oracle/ComparatorTest.php",
79+
"Functional/Schema/OracleSchemaManagerTest.php",
80+
"Functional/Schema/PostgreSQL/ComparatorTest.php",
81+
"Functional/Schema/PostgreSQL/SchemaTest.php",
82+
"Functional/Schema/PostgreSQLSchemaManagerTest.php",
83+
"Functional/Schema/SchemaManagerTest.php",
84+
"Functional/Schema/SQLite/ComparatorTest.php",
85+
"Functional/Schema/SQLiteSchemaManagerTest.php",
86+
"Functional/Schema/SQLServerSchemaManagerTest.php",
6587
]);
6688

6789
if (!existsSync(doctrineTestsDir)) {

scripts/run-functional-tests.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const vitestArgs = [
2929
"x",
3030
"vitest",
3131
"run",
32+
"--fileParallelism=false",
3233
"src/__tests__/functional",
3334
"--exclude",
3435
"src/__tests__/functional/_helpers/**/*.test.ts",

src/__tests__/functional/_helpers/functional-connection-factory.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,15 @@ async function createMySQL2DriverManagerParams(
240240
): Promise<ConnectionParams> {
241241
const mysql2 = await importOptional("mysql2/promise", target);
242242
const mysqlModule = mysql2.default ?? mysql2;
243+
const charset = readEnv(target.platform, "CHARSET", "utf8mb4", role);
244+
const collation = readEnv(target.platform, "COLLATION", "utf8mb4_general_ci", role);
243245

244246
const config = {
245-
bigNumberStrings: true,
247+
bigNumberStrings: false,
248+
charset,
246249
database: readEnv(target.platform, "DATABASE", "datazen", role),
247250
host: readEnv(target.platform, "HOST", "127.0.0.1", role),
251+
jsonStrings: true,
248252
password: readEnv(
249253
target.platform,
250254
"PASSWORD",
@@ -264,6 +268,8 @@ async function createMySQL2DriverManagerParams(
264268
const connection = await mysqlModule.createConnection(config);
265269

266270
return {
271+
charset,
272+
collation,
267273
database: config.database,
268274
dbname: config.database,
269275
host: config.host,
@@ -284,6 +290,8 @@ async function createMySQL2DriverManagerParams(
284290
const pool = mysqlModule.createPool(config);
285291

286292
return {
293+
charset,
294+
collation,
287295
database: config.database,
288296
dbname: config.database,
289297
host: config.host,
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { beforeEach, describe, expect, it } from "vitest";
2+
3+
import { ParameterType } from "../../../parameter-type";
4+
import { DB2Platform } from "../../../platforms/db2-platform";
5+
import { NotSupported } from "../../../platforms/exception/not-supported";
6+
import { MariaDBPlatform } from "../../../platforms/mariadb-platform";
7+
import { MariaDB1060Platform } from "../../../platforms/mariadb1060-platform";
8+
import { MySQLPlatform } from "../../../platforms/mysql-platform";
9+
import { MySQL80Platform } from "../../../platforms/mysql80-platform";
10+
import { OraclePlatform } from "../../../platforms/oracle-platform";
11+
import { SQLitePlatform } from "../../../platforms/sqlite-platform";
12+
import { ConflictResolutionMode } from "../../../query/for-update/conflict-resolution-mode";
13+
import { UnionType } from "../../../query/union-type";
14+
import { Column } from "../../../schema/column";
15+
import { PrimaryKeyConstraint } from "../../../schema/primary-key-constraint";
16+
import { Table } from "../../../schema/table";
17+
import { Types } from "../../../types/types";
18+
import { useFunctionalTestCase } from "../_helpers/functional-test-case";
19+
20+
describe("Functional/Query/QueryBuilderTest", () => {
21+
const functional = useFunctionalTestCase();
22+
23+
beforeEach(async () => {
24+
const table = Table.editor()
25+
.setUnquotedName("for_update")
26+
.setColumns(Column.editor().setUnquotedName("id").setTypeName(Types.INTEGER).create())
27+
.setPrimaryKeyConstraint(PrimaryKeyConstraint.editor().setUnquotedColumnNames("id").create())
28+
.create();
29+
30+
await functional.dropAndCreateTable(table);
31+
await functional.connection().insert("for_update", { id: 1 });
32+
await functional.connection().insert("for_update", { id: 2 });
33+
});
34+
35+
it("for update ordinary", async ({ skip }) => {
36+
const connection = functional.connection();
37+
if (connection.getDatabasePlatform() instanceof SQLitePlatform) {
38+
skip();
39+
}
40+
41+
const qb = connection.createQueryBuilder();
42+
qb.select("id").from("for_update").forUpdate();
43+
44+
expect(await qb.fetchFirstColumn()).toEqual([1, 2]);
45+
});
46+
47+
it("for update skip locked when supported", async ({ skip }) => {
48+
const connection = functional.connection();
49+
if (!platformSupportsSkipLocked(connection.getDatabasePlatform())) {
50+
skip();
51+
}
52+
53+
const qb1 = connection.createQueryBuilder();
54+
qb1.select("id").from("for_update").where("id = 1").forUpdate();
55+
56+
await connection.beginTransaction();
57+
expect(await qb1.fetchFirstColumn()).toEqual([1]);
58+
59+
const connection2 = await functional.createConnection();
60+
61+
try {
62+
const qb2 = connection2.createQueryBuilder();
63+
qb2
64+
.select("id")
65+
.from("for_update")
66+
.orderBy("id")
67+
.forUpdate(ConflictResolutionMode.SKIP_LOCKED);
68+
69+
expect(await qb2.fetchFirstColumn()).toEqual([2]);
70+
} finally {
71+
await connection2.close();
72+
if (connection.isTransactionActive()) {
73+
await connection.rollBack();
74+
}
75+
}
76+
});
77+
78+
it("for update skip locked when not supported", async ({ skip }) => {
79+
const connection = functional.connection();
80+
if (platformSupportsSkipLocked(connection.getDatabasePlatform())) {
81+
skip();
82+
}
83+
84+
const qb = connection.createQueryBuilder();
85+
qb.select("id").from("for_update").forUpdate(ConflictResolutionMode.SKIP_LOCKED);
86+
87+
await expect(qb.executeQuery()).rejects.toThrow();
88+
});
89+
90+
it("union all and distinct return expected results", async () => {
91+
const connection = functional.connection();
92+
const platform = connection.getDatabasePlatform();
93+
94+
const qbAll = connection.createQueryBuilder();
95+
qbAll
96+
.union(platform.getDummySelectSQL("2 as field_one"))
97+
.addUnion(platform.getDummySelectSQL("1 as field_one"), UnionType.ALL)
98+
.addUnion(platform.getDummySelectSQL("1 as field_one"), UnionType.ALL)
99+
.orderBy("field_one", "ASC");
100+
101+
const qbDistinct = connection.createQueryBuilder();
102+
qbDistinct
103+
.union(platform.getDummySelectSQL("2 as field_one"))
104+
.addUnion(platform.getDummySelectSQL("1 as field_one"), UnionType.DISTINCT)
105+
.addUnion(platform.getDummySelectSQL("1 as field_one"), UnionType.DISTINCT)
106+
.orderBy("field_one", "ASC");
107+
108+
const allRows = normalizeNumericRows(
109+
await qbAll.executeQuery().then((result) => result.fetchAllAssociative()),
110+
);
111+
const distinctRows = normalizeNumericRows(
112+
await qbDistinct.executeQuery().then((result) => result.fetchAllAssociative()),
113+
);
114+
115+
expect(allRows).toEqual([{ field_one: 1 }, { field_one: 1 }, { field_one: 2 }]);
116+
expect(distinctRows).toEqual([{ field_one: 1 }, { field_one: 2 }]);
117+
});
118+
119+
it("union and addUnion work with query builder parts and named parameters", async () => {
120+
const connection = functional.connection();
121+
const qb = connection.createQueryBuilder();
122+
123+
const sub1 = qb
124+
.sub()
125+
.select("id")
126+
.from("for_update")
127+
.where(qb.expr().eq("id", qb.createNamedParameter(1, ParameterType.INTEGER)));
128+
const sub2 = qb
129+
.sub()
130+
.select("id")
131+
.from("for_update")
132+
.where(qb.expr().eq("id", qb.createNamedParameter(2, ParameterType.INTEGER)));
133+
const sub3 = qb
134+
.sub()
135+
.select("id")
136+
.from("for_update")
137+
.where(qb.expr().eq("id", qb.createNamedParameter(1, ParameterType.INTEGER)));
138+
139+
qb.union(sub1)
140+
.addUnion(sub2, UnionType.DISTINCT)
141+
.addUnion(sub3, UnionType.DISTINCT)
142+
.orderBy("id", "DESC");
143+
144+
const rows = normalizeNumericRows(
145+
await qb.executeQuery().then((result) => result.fetchAllAssociative()),
146+
);
147+
148+
expect(rows).toEqual([{ id: 2 }, { id: 1 }]);
149+
});
150+
151+
it("select with CTE named parameter", async ({ skip }) => {
152+
const connection = functional.connection();
153+
const platform = connection.getDatabasePlatform();
154+
if (!platformSupportsCTEs(platform) || !platformSupportsCTEColumnsDefinition(platform)) {
155+
skip();
156+
}
157+
158+
const qb = connection.createQueryBuilder();
159+
const cteQueryBuilder = qb
160+
.sub()
161+
.select("id AS virtual_id")
162+
.from("for_update")
163+
.where("id = :id");
164+
165+
qb.with("cte_a", cteQueryBuilder, ["virtual_id"])
166+
.select("virtual_id")
167+
.from("cte_a")
168+
.setParameter("id", 1);
169+
170+
const rows = normalizeNumericRows(
171+
await qb.executeQuery().then((result) => result.fetchAllAssociative()),
172+
);
173+
174+
expect(rows).toEqual([{ virtual_id: 1 }]);
175+
});
176+
177+
it("platform does not support CTE", async ({ skip }) => {
178+
const connection = functional.connection();
179+
if (platformSupportsCTEs(connection.getDatabasePlatform())) {
180+
skip();
181+
}
182+
183+
const qb = connection.createQueryBuilder();
184+
const cteQueryBuilder = qb.sub().select("id").from("for_update");
185+
qb.with("cte_a", cteQueryBuilder).select("id").from("cte_a");
186+
187+
await expect(qb.executeQuery()).rejects.toThrow(NotSupported);
188+
});
189+
});
190+
191+
function normalizeNumericRows(
192+
rows: Array<Record<string, unknown>>,
193+
): Array<Record<string, number | string | null>> {
194+
return rows.map((row) =>
195+
Object.fromEntries(
196+
Object.entries(row).map(([key, value]) => [
197+
key.toLowerCase(),
198+
typeof value === "number"
199+
? value
200+
: typeof value === "bigint"
201+
? Number(value)
202+
: typeof value === "string" && /^-?\d+$/.test(value)
203+
? Number(value)
204+
: (value as string | null),
205+
]),
206+
),
207+
);
208+
}
209+
210+
function platformSupportsSkipLocked(platform: unknown): boolean {
211+
if (platform instanceof DB2Platform) {
212+
return false;
213+
}
214+
215+
if (platform instanceof MySQLPlatform) {
216+
return platform instanceof MySQL80Platform;
217+
}
218+
219+
if (platform instanceof MariaDBPlatform) {
220+
return platform instanceof MariaDB1060Platform;
221+
}
222+
223+
return !(platform instanceof SQLitePlatform);
224+
}
225+
226+
function platformSupportsCTEs(platform: unknown): boolean {
227+
return !(platform instanceof MySQLPlatform) || platform instanceof MySQL80Platform;
228+
}
229+
230+
function platformSupportsCTEColumnsDefinition(platform: unknown): boolean {
231+
if (platform instanceof DB2Platform || platform instanceof OraclePlatform) {
232+
return false;
233+
}
234+
235+
return !(platform instanceof MySQLPlatform) || platform instanceof MySQL80Platform;
236+
}

0 commit comments

Comments
 (0)