Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

# Final Doctrine Parity & Production-Ready Release

## 1.1.2

- Refactored `fetchNumeric()`, `fetchAssociative()`, and `fetchOne()` across `Result`, `Connection`, `QueryBuilder`, portability wrappers, and all bundled drivers to return `undefined` instead of `false` when no row is available, and aligned unit tests and metadata-provider integrations with the new contract.
- Normalized bound query parameter values so `undefined` is treated as SQL `NULL` (`null`) before hitting driver statements, preventing mysql2 bind errors and aligning positional and named parameter flows.
- Updated remaining functional tests to assert `undefined` (instead of `false`) for empty single-row `fetch*` results and no-row guards.

# 1.1.1

- Added package subpath entry points like `@devscast/datazen/logging` that resolve directly from published `dist/*` artifacts, including compatibility for older TypeScript `moduleResolution: "node"` projects without shipping extra root wrapper folders.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devscast/datazen",
"version": "1.1.1",
"version": "1.1.2",
"type": "module",
"source": "./src/_index.ts",
"main": "./dist/index.cjs",
Expand Down
21 changes: 21 additions & 0 deletions src/__tests__/connection/connection-parameter-compilation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@ class NamedSpyDriver implements Driver {
}

describe("Connection parameter compilation", () => {
it("treats undefined named parameters as SQL NULL", async () => {
const capture = new CaptureConnection();
const connection = new Connection({}, new NamedSpyDriver(capture));

await connection.executeQuery(
"SELECT * FROM users WHERE deleted_at = :deletedAt",
{ deletedAt: undefined },
{ deletedAt: ParameterType.STRING },
);

expect(capture.latestQuery).toEqual({
parameters: {
p1: null,
},
sql: "SELECT * FROM users WHERE deleted_at = @p1",
types: {
p1: ParameterType.STRING,
},
});
});

it("compiles named placeholders to sqlserver style named bindings", async () => {
const capture = new CaptureConnection();
const connection = new Connection({}, new NamedSpyDriver(capture));
Expand Down
10 changes: 10 additions & 0 deletions src/__tests__/connection/connection-type-conversion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ class SpyDriver implements Driver {
describe("Connection type conversion", () => {
registerBuiltInTypes();

it("treats undefined positional parameters as SQL NULL", async () => {
const capture = new CaptureConnection();
const connection = new Connection({}, new SpyDriver(capture));

await connection.executeQuery("SELECT ? AS maybe", [undefined], [ParameterType.STRING]);

expect(capture.latestQuery?.parameters).toEqual([null]);
expect(capture.latestQuery?.types).toEqual([ParameterType.STRING]);
});

it("converts Datazen Type names to driver values and binding types", async () => {
const capture = new CaptureConnection();
const connection = new Connection({}, new SpyDriver(capture));
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/connection/connection-typed-fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class StaticRowsDriver implements Driver {
}
}

function expectUserRow(_row: { id: number; name: string } | false): void {}
function expectUserRow(_row: { id: number; name: string } | undefined): void {}

describe("Connection typed fetch", () => {
it("propagates row type through executeQuery<T>() and Result<T>", async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/driver/driver-result-classes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe("driver result classes", () => {

expect(result.fetchOne<number>()).toBe(1);
expect(result.fetchNumeric<[number, string]>()).toEqual([2, "Bob"]);
expect(result.fetchAssociative()).toBe(false);
expect(result.fetchAssociative()).toBeUndefined();
expect(result.rowCount()).toBe(2);
expect(result.columnCount()).toBe(2);
expect(
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/driver/fetch-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ describe("FetchUtils parity helpers", () => {
10, 20,
]);
expect(FetchUtils.fetchOne(new ArrayResult([{ id: 99 }]))).toBe(99);
expect(FetchUtils.fetchOne(new ArrayResult([]))).toBe(false);
expect(FetchUtils.fetchOne(new ArrayResult([]))).toBeUndefined();
});
});
2 changes: 1 addition & 1 deletion src/__tests__/functional/auto-increment-column.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe("Functional/AutoIncrementColumnTest", () => {

async function maxId(connection: Connection): Promise<number> {
const value = await connection.fetchOne("SELECT MAX(id) FROM auto_increment_table");
expect(value).not.toBe(false);
expect(value).toBeDefined();

return Number(value);
}
Expand Down
14 changes: 7 additions & 7 deletions src/__tests__/functional/binary-data-access.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe("Functional/BinaryDataAccessTest", () => {
stmt.bindValue(2, Buffer.from("c0def00d", "hex"), ParameterType.BINARY);

const row = lowerCaseKeys((await stmt.executeQuery()).fetchAssociative());
expect(row).not.toBe(false);
expect(row).toBeDefined();
expect(Object.keys(row as Record<string, unknown>)).toEqual(["test_int", "test_binary"]);
expect((row as Record<string, unknown>).test_int).toBe(1);
expect(toBinaryBuffer((row as Record<string, unknown>).test_binary)).toEqual(
Expand Down Expand Up @@ -130,7 +130,7 @@ describe("Functional/BinaryDataAccessTest", () => {
{ 1: ParameterType.BINARY },
);

expect(row).not.toBe(false);
expect(row).toBeDefined();
const normalized = lowerCaseKeys(row);
expect(normalized.test_int).toBe(1);
expect(toBinaryBuffer(normalized.test_binary)).toEqual(Buffer.from("c0def00d", "hex"));
Expand All @@ -145,7 +145,7 @@ describe("Functional/BinaryDataAccessTest", () => {
[ParameterType.STRING, Types.BINARY],
);

expect(row).not.toBe(false);
expect(row).toBeDefined();
const normalized = lowerCaseKeys(row);
expect(normalized.test_int).toBe(1);
expect(toBinaryBuffer(normalized.test_binary)).toEqual(Buffer.from("c0def00d", "hex"));
Expand All @@ -160,7 +160,7 @@ describe("Functional/BinaryDataAccessTest", () => {
{ 1: ParameterType.BINARY },
);

expect(row).not.toBe(false);
expect(row).toBeDefined();
expect(row?.[0]).toBe(1);
expect(toBinaryBuffer(row?.[1])).toEqual(Buffer.from("c0def00d", "hex"));
});
Expand All @@ -174,7 +174,7 @@ describe("Functional/BinaryDataAccessTest", () => {
[ParameterType.STRING, Types.BINARY],
);

expect(row).not.toBe(false);
expect(row).toBeDefined();
expect(row?.[0]).toBe(1);
expect(toBinaryBuffer(row?.[1])).toEqual(Buffer.from("c0def00d", "hex"));
});
Expand Down Expand Up @@ -255,8 +255,8 @@ describe("Functional/BinaryDataAccessTest", () => {
});
});

function lowerCaseKeys(row: false | Record<string, unknown> | undefined): Record<string, unknown> {
if (row === false || row === undefined) {
function lowerCaseKeys(row: Record<string, unknown> | undefined): Record<string, unknown> {
if (row === undefined) {
throw new Error("Expected a row.");
}

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/functional/blob.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe("Functional/BlobTest", () => {
const blobs = await functional
.connection()
.fetchNumeric("SELECT blobcolumn1, blobcolumn2 FROM blob_table");
expect(blobs).not.toBe(false);
expect(blobs).toBeDefined();

const actual = (blobs ?? []).map((blob) => toText(blob));
expect(actual).toEqual(["test1", "test2"]);
Expand Down
12 changes: 6 additions & 6 deletions src/__tests__/functional/connection/fetch-empty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ describe("Functional/Connection/FetchEmptyTest", () => {
query = `SELECT * FROM (${connection.getDatabasePlatform().getDummySelectSQL("1 c")}) t WHERE 1 = 0`;
});

it("returns false for fetchAssociative()", async () => {
expect(await connection.fetchAssociative(query)).toBe(false);
it("returns undefined for fetchAssociative()", async () => {
expect(await connection.fetchAssociative(query)).toBeUndefined();
});

it("returns false for fetchNumeric()", async () => {
expect(await connection.fetchNumeric(query)).toBe(false);
it("returns undefined for fetchNumeric()", async () => {
expect(await connection.fetchNumeric(query)).toBeUndefined();
});

it("returns false for fetchOne()", async () => {
expect(await connection.fetchOne(query)).toBe(false);
it("returns undefined for fetchOne()", async () => {
expect(await connection.fetchOne(query)).toBeUndefined();
});

it("returns an empty array for fetchAllAssociative()", async () => {
Expand Down
14 changes: 7 additions & 7 deletions src/__tests__/functional/data-access.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe("Functional/DataAccessTest", () => {
[1, "foo"],
);

expect(row).not.toBe(false);
expect(row).toBeDefined();
const normalized = lowerCaseKeys(row);
expect(normalized.test_int).toBe(1);
expect(normalized.test_string).toBe("foo");
Expand All @@ -146,7 +146,7 @@ describe("Functional/DataAccessTest", () => {
[ParameterType.STRING, Types.DATETIME_MUTABLE],
);

expect(row).not.toBe(false);
expect(row).toBeDefined();
const normalized = lowerCaseKeys(row);
expect(normalized.test_int).toBe(1);
expect(normalizeDateTimeSecondPrecision(normalized.test_datetime)).toBe("2010-01-01 10:10:10");
Expand All @@ -160,7 +160,7 @@ describe("Functional/DataAccessTest", () => {
[1, "foo"],
);

expect(row).not.toBe(false);
expect(row).toBeDefined();
expect(row?.[0]).toBe(1);
expect(row?.[1]).toBe("foo");
});
Expand All @@ -176,7 +176,7 @@ describe("Functional/DataAccessTest", () => {
[ParameterType.STRING, Types.DATETIME_MUTABLE],
);

expect(row).not.toBe(false);
expect(row).toBeDefined();
expect(row?.[0]).toBe(1);
expect(normalizeDateTimeSecondPrecision(row?.[1])).toBe("2010-01-01 10:10:10");
});
Expand Down Expand Up @@ -420,12 +420,12 @@ async function assertDateExpression(
bindParams(stmt, interval);

const date = (await stmt.executeQuery()).fetchOne();
expect(date).not.toBe(false);
expect(date).toBeDefined();
expect(normalizeDateTimeSecondPrecision(date)).toBe(expected);
}

function lowerCaseKeys(row: false | Record<string, unknown> | undefined): Record<string, unknown> {
if (row === false || row === undefined) {
function lowerCaseKeys(row: Record<string, unknown> | undefined): Record<string, unknown> {
if (row === undefined) {
throw new Error("Expected a row.");
}

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/functional/lock-mode/none.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe("Functional/LockMode/NoneTest", () => {
let query = "SELECT id FROM users";
query = connection2.getDatabasePlatform().appendLockHint(query, LockMode.NONE);

expect(await connection2.fetchOne(query)).toBe(false);
expect(await connection2.fetchOne(query)).toBeUndefined();
} finally {
while (connection2.isTransactionActive()) {
await connection2.rollBack();
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/functional/platform/alter-column.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe("Functional/Platform/AlterColumnTest", () => {
const hasIcuCollations =
(await functional
.connection()
.fetchOne("SELECT 1 FROM pg_collation WHERE collprovider = 'icu'")) !== false;
.fetchOne("SELECT 1 FROM pg_collation WHERE collprovider = 'icu'")) !== undefined;
if (!hasIcuCollations) {
skip();
}
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/functional/platform/default-expression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ async function assertDefaultExpression(
const row = await connection.fetchNumeric<[unknown, unknown]>(
"SELECT default_value, actual_value FROM default_expr_test",
);
expect(row).not.toBe(false);
if (row === false) {
expect(row).toBeDefined();
if (row === undefined) {
return;
}

Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/functional/platform/quoting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ describe("Functional/Platform/QuotingTest", () => {
);
const row = await functional.connection().fetchAssociative(query);

expect(row).not.toBe(false);
if (row === false) {
expect(row).toBeDefined();
if (row === undefined) {
return;
}

Expand Down
8 changes: 4 additions & 4 deletions src/__tests__/functional/portability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ describe("Functional/PortabilityTest", () => {
assertFetchResultRows(rows);

let result = await connection.executeQuery("SELECT * FROM portability_table");
for (let row = result.fetchAssociative(); row !== false; row = result.fetchAssociative()) {
for (let row = result.fetchAssociative(); row !== undefined; row = result.fetchAssociative()) {
assertFetchResultRow(row);
}

result = await (await connection.prepare("SELECT * FROM portability_table")).executeQuery();
for (let row = result.fetchAssociative(); row !== false; row = result.fetchAssociative()) {
for (let row = result.fetchAssociative(); row !== undefined; row = result.fetchAssociative()) {
assertFetchResultRow(row);
}
});
Expand All @@ -58,8 +58,8 @@ describe("Functional/PortabilityTest", () => {
await createPortabilityTable(connection);

const row = await connection.fetchAssociative("SELECT * FROM portability_table");
expect(row).not.toBe(false);
if (row === false) {
expect(row).toBeDefined();
if (row === undefined) {
return;
}

Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/functional/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ describe("Functional/ResultTest", () => {
[
"fetchNumeric",
(result: Awaited<ReturnType<typeof connection.executeQuery>>) => result.fetchNumeric(),
false,
undefined,
],
[
"fetchAssociative",
(result: Awaited<ReturnType<typeof connection.executeQuery>>) => result.fetchAssociative(),
false,
undefined,
],
[
"fetchOne",
(result: Awaited<ReturnType<typeof connection.executeQuery>>) => result.fetchOne(),
false,
undefined,
],
[
"fetchAllNumeric",
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/functional/schema/postgre-sql/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ describe("Functional/Schema/PostgreSQL/SchemaTest", () => {
["my_table"],
);

expect(row).not.toBe(false);
if (row === false) {
expect(row).toBeDefined();
if (row === undefined) {
return;
}

Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/functional/transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ async function killCurrentSession(
}

const currentProcessId = await connection.fetchOne(currentProcessQuery);
expect(currentProcessId).not.toBe(false);
if (currentProcessId === false) {
expect(currentProcessId).toBeDefined();
if (currentProcessId === undefined) {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/functional/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ describe("Functional/WriteTest", () => {

expect(
await connection.fetchOne("SELECT test_string FROM write_table WHERE test_int = 30"),
).toBe(false);
).toBeUndefined();
});

it("supports empty identity insert SQL", async () => {
Expand Down
9 changes: 6 additions & 3 deletions src/__tests__/platforms/metadata-providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ class StubAsyncQueryConnection {
private readonly columns: Record<string, unknown[]> = {},
private readonly numerics: Record<string, unknown[][]> = {},
private readonly associatives: Record<string, Record<string, unknown>[]> = {},
private readonly fetchOneValue: unknown = false,
private readonly fetchOneValue: unknown = undefined,
) {}

public async fetchOne<T = unknown>(_sql: string, _params: unknown[] = []): Promise<T | false> {
return this.fetchOneValue as T | false;
public async fetchOne<T = unknown>(
_sql: string,
_params: unknown[] = [],
): Promise<T | undefined> {
return this.fetchOneValue as T | undefined;
}

public async fetchFirstColumn<T = unknown>(sql: string, _params: unknown[] = []): Promise<T[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe("MySQL ConnectionCharsetMetadataProvider", () => {

it("returns null when the charset is not found", async () => {
const provider = new ConnectionCharsetMetadataProvider({
fetchOne: async () => false,
fetchOne: async () => undefined,
});

await expect(provider.getDefaultCharsetCollation("missing")).resolves.toBeNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("MySQL ConnectionCollationMetadataProvider", () => {

it("returns null when the collation is not found", async () => {
const provider = new ConnectionCollationMetadataProvider({
fetchOne: async () => false,
fetchOne: async () => undefined,
});

await expect(provider.getCollationCharset("missing")).resolves.toBeNull();
Expand Down
Loading
Loading