connection: rewrote query execution, batching, added generic sqlite, added node/bun sqlite, aligned repo/mutator results

This commit is contained in:
dswbx
2025-06-12 09:02:18 +02:00
parent 88419548c7
commit 6c2e579596
40 changed files with 990 additions and 649 deletions

View File

@@ -1,4 +1,4 @@
import { afterAll, afterEach, describe, expect, test } from "bun:test";
import { afterEach, describe, test } from "bun:test";
import { App } from "../src";
import { getDummyConnection } from "./helper";

View File

@@ -153,7 +153,7 @@ describe("DataApi", () => {
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
const oneByRes = await oneBy;
expect(oneByRes.data).toEqual({ title: "baz" } as any);
expect(oneByRes.body.meta.count).toEqual(1);
expect(oneByRes.body.meta.items).toEqual(1);
});
it("exists/count", async () => {

View File

@@ -7,13 +7,13 @@ import {
type EntityData,
EntityManager,
ManyToOneRelation,
type MutatorResponse,
type RepositoryResponse,
TextField,
} from "../../src/data";
import { DataController } from "../../src/data/api/DataController";
import { dataConfigSchema } from "../../src/data/data-schema";
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
const { dummyConnection, afterAllCleanup } = getDummyConnection();
beforeAll(() => disableConsoleLog(["log", "warn"]));
@@ -21,52 +21,6 @@ afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
const dataConfig = parse(dataConfigSchema, {});
describe("[data] DataController", async () => {
test("repoResult", async () => {
const em = new EntityManager<any>([], dummyConnection);
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const res = controller.repoResult({
entity: null as any,
data: [] as any,
sql: "",
parameters: [] as any,
result: [] as any,
meta: {
total: 0,
count: 0,
items: 0,
},
});
expect(res).toEqual({
meta: {
total: 0,
count: 0,
items: 0,
},
data: [],
});
});
test("mutatorResult", async () => {
const em = new EntityManager([], dummyConnection);
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const res = controller.mutatorResult({
entity: null as any,
data: [] as any,
sql: "",
parameters: [] as any,
result: [] as any,
});
expect(res).toEqual({
data: [],
});
});
describe("getController", async () => {
const users = new Entity("users", [
new TextField("name", { required: true }),
@@ -120,8 +74,7 @@ describe("[data] DataController", async () => {
method: "POST",
body: JSON.stringify(_user),
});
//console.log("res", { _user }, res);
const result = (await res.json()) as MutatorResponse;
const result = (await res.json()) as MutatorResultJSON;
const { id, ...data } = result.data as any;
expect(res.status).toBe(201);
@@ -135,7 +88,7 @@ describe("[data] DataController", async () => {
method: "POST",
body: JSON.stringify(_post),
});
const result = (await res.json()) as MutatorResponse;
const result = (await res.json()) as MutatorResultJSON;
const { id, ...data } = result.data as any;
expect(res.status).toBe(201);
@@ -146,13 +99,13 @@ describe("[data] DataController", async () => {
test("/:entity (read many)", async () => {
const res = await app.request("/entity/users");
const data = (await res.json()) as RepositoryResponse;
const data = (await res.json()) as RepositoryResultJSON;
expect(data.meta.total).toBe(3);
expect(data.meta.count).toBe(3);
//expect(data.meta.total).toBe(3);
//expect(data.meta.count).toBe(3);
expect(data.meta.items).toBe(3);
expect(data.data.length).toBe(3);
expect(data.data[0].name).toBe("foo");
expect(data.data[0]?.name).toBe("foo");
});
test("/:entity/query (func query)", async () => {
@@ -165,33 +118,32 @@ describe("[data] DataController", async () => {
where: { bio: { $isnull: 1 } },
}),
});
const data = (await res.json()) as RepositoryResponse;
const data = (await res.json()) as RepositoryResultJSON;
expect(data.meta.total).toBe(3);
expect(data.meta.count).toBe(1);
//expect(data.meta.total).toBe(3);
//expect(data.meta.count).toBe(1);
expect(data.meta.items).toBe(1);
expect(data.data.length).toBe(1);
expect(data.data[0].name).toBe("bar");
expect(data.data[0]?.name).toBe("bar");
});
test("/:entity (read many, paginated)", async () => {
const res = await app.request("/entity/users?limit=1&offset=2");
const data = (await res.json()) as RepositoryResponse;
const data = (await res.json()) as RepositoryResultJSON;
expect(data.meta.total).toBe(3);
expect(data.meta.count).toBe(3);
//expect(data.meta.total).toBe(3);
//expect(data.meta.count).toBe(3);
expect(data.meta.items).toBe(1);
expect(data.data.length).toBe(1);
expect(data.data[0].name).toBe("baz");
expect(data.data[0]?.name).toBe("baz");
});
test("/:entity/:id (read one)", async () => {
const res = await app.request("/entity/users/3");
const data = (await res.json()) as RepositoryResponse<EntityData>;
console.log("data", data);
const data = (await res.json()) as RepositoryResultJSON<EntityData>;
expect(data.meta.total).toBe(3);
expect(data.meta.count).toBe(1);
//expect(data.meta.total).toBe(3);
//expect(data.meta.count).toBe(1);
expect(data.meta.items).toBe(1);
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
});
@@ -201,7 +153,7 @@ describe("[data] DataController", async () => {
method: "PATCH",
body: JSON.stringify({ name: "new name" }),
});
const { data } = (await res.json()) as MutatorResponse;
const { data } = (await res.json()) as MutatorResultJSON;
expect(res.ok).toBe(true);
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
@@ -209,27 +161,26 @@ describe("[data] DataController", async () => {
test("/:entity/:id/:reference (read references)", async () => {
const res = await app.request("/entity/users/1/posts");
const data = (await res.json()) as RepositoryResponse;
console.log("data", data);
const data = (await res.json()) as RepositoryResultJSON;
expect(data.meta.total).toBe(2);
expect(data.meta.count).toBe(1);
//expect(data.meta.total).toBe(2);
//expect(data.meta.count).toBe(1);
expect(data.meta.items).toBe(1);
expect(data.data.length).toBe(1);
expect(data.data[0].content).toBe("post 1");
expect(data.data[0]?.content).toBe("post 1");
});
test("/:entity/:id (delete one)", async () => {
const res = await app.request("/entity/posts/2", {
method: "DELETE",
});
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
const { data } = (await res.json()) as RepositoryResultJSON<EntityData>;
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
// verify
const res2 = await app.request("/entity/posts");
const data2 = (await res2.json()) as RepositoryResponse;
expect(data2.meta.total).toBe(1);
const data2 = (await res2.json()) as RepositoryResultJSON;
//expect(data2.meta.total).toBe(1);
});
});
});

View File

@@ -34,19 +34,12 @@ describe("some tests", async () => {
test("findId", async () => {
const query = await em.repository(users).findId(1);
/*const { result, total, count, time } = query;
console.log("query", query.result, {
result,
total,
count,
time,
});*/
expect(query.sql).toBe(
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
);
expect(query.parameters).toEqual([1, 1]);
expect(query.result).toEqual([]);
expect(query.data).toBeUndefined();
});
test("findMany", async () => {
@@ -56,7 +49,7 @@ describe("some tests", async () => {
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
);
expect(query.parameters).toEqual([10, 0]);
expect(query.result).toEqual([]);
expect(query.data).toEqual([]);
});
test("findMany with number", async () => {
@@ -66,7 +59,7 @@ describe("some tests", async () => {
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
);
expect(query.parameters).toEqual([10, 0]);
expect(query.result).toEqual([]);
expect(query.data).toEqual([]);
});
test("try adding an existing field name", async () => {

View File

@@ -45,7 +45,7 @@ describe("Mutator simple", async () => {
},
});
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
expect(query.data).toEqual([{ id: 1, label: "test", count: 1 }]);
});
test("update inserted row", async () => {
@@ -87,7 +87,7 @@ describe("Mutator simple", async () => {
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
const query2 = await em.repository(items).findId(id);
expect(query2.result.length).toBe(0);
expect(query2.data).toBeUndefined();
});
test("validation: insert incomplete row", async () => {
@@ -177,13 +177,13 @@ describe("Mutator simple", async () => {
});
test("insertMany", async () => {
const oldCount = (await em.repo(items).count()).count;
const oldCount = (await em.repo(items).count()).data.count;
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
const { data } = await em.mutator(items).insertMany(inserts);
expect(data.length).toBe(2);
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
const newCount = (await em.repo(items).count()).count;
const newCount = (await em.repo(items).count()).data.count;
expect(newCount).toBe(oldCount + inserts.length);
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });

View File

@@ -1,4 +1,4 @@
import { afterAll, describe, expect, test } from "bun:test";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { EventManager } from "../../../src/core/events";
import {
Entity,
@@ -12,11 +12,14 @@ import {
TextField,
} from "../../../src/data";
import * as proto from "../../../src/data/prototype";
import { getDummyConnection } from "../helper";
import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterAll(afterAllCleanup);
beforeAll(() => disableConsoleLog(["log", "warn"]));
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
describe("[data] Mutator (base)", async () => {
const entity = new Entity("items", [
new TextField("label", { required: true }),

View File

@@ -26,120 +26,6 @@ async function sleep(ms: number) {
}
describe("[Repository]", async () => {
test.skip("bulk", async () => {
//const connection = dummyConnection;
//const connection = getLocalLibsqlConnection();
const credentials = null as any; // @todo: determine what to do here
const connection = new LibsqlConnection(credentials);
const em = new EntityManager([], connection);
/*const emLibsql = new EntityManager([], {
url: connection.url.replace("https", "libsql"),
authToken: connection.authToken,
});*/
const table = "posts";
const client = connection.getClient();
if (!client) {
console.log("Cannot perform test without libsql connection");
return;
}
const conn = em.connection.kysely;
const selectQ = (e: E) => e.selectFrom(table).selectAll().limit(2);
const countQ = (e: E) => e.selectFrom(table).select(e.fn.count("*").as("count"));
async function executeTransaction(em: EntityManager<any>) {
return await em.connection.kysely.transaction().execute(async (e) => {
const res = await selectQ(e).execute();
const count = await countQ(e).execute();
return [res, count];
});
}
async function executeBatch(em: EntityManager<any>) {
const queries = [selectQ(conn), countQ(conn)];
return await em.connection.batchQuery(queries);
}
async function executeSingleKysely(em: EntityManager<any>) {
const res = await selectQ(conn).execute();
const count = await countQ(conn).execute();
return [res, count];
}
async function executeSingleClient(em: EntityManager<any>) {
const q1 = selectQ(conn).compile();
const res = await client.execute({
sql: q1.sql,
args: q1.parameters as any,
});
const q2 = countQ(conn).compile();
const count = await client.execute({
sql: q2.sql,
args: q2.parameters as any,
});
return [res, count];
}
const transaction = await executeTransaction(em);
const batch = await executeBatch(em);
expect(batch).toEqual(transaction as any);
const testperf = false;
if (testperf) {
const times = 5;
const exec = async (
name: string,
fn: (em: EntityManager<any>) => Promise<any>,
em: EntityManager<any>,
) => {
const res = await Perf.execute(() => fn(em), times);
await sleep(1000);
const info = {
name,
total: res.total.toFixed(2),
avg: (res.total / times).toFixed(2),
first: res.marks[0].time.toFixed(2),
last: res.marks[res.marks.length - 1].time.toFixed(2),
};
console.log(info.name, info, res.marks);
return info;
};
const data: any[] = [];
data.push(await exec("transaction.http", executeTransaction, em));
data.push(await exec("bulk.http", executeBatch, em));
data.push(await exec("singleKy.http", executeSingleKysely, em));
data.push(await exec("singleCl.http", executeSingleClient, em));
/*data.push(await exec("transaction.libsql", executeTransaction, emLibsql));
data.push(await exec("bulk.libsql", executeBatch, emLibsql));
data.push(await exec("singleKy.libsql", executeSingleKysely, emLibsql));
data.push(await exec("singleCl.libsql", executeSingleClient, emLibsql));*/
console.table(data);
/**
* ┌───┬────────────────────┬────────┬────────┬────────┬────────┐
* │ │ name │ total │ avg │ first │ last │
* ├───┼────────────────────┼────────┼────────┼────────┼────────┤
* │ 0 │ transaction.http │ 681.29 │ 136.26 │ 136.46 │ 396.09 │
* │ 1 │ bulk.http │ 164.82 │ 32.96 │ 32.95 │ 99.91 │
* │ 2 │ singleKy.http │ 330.01 │ 66.00 │ 65.86 │ 195.41 │
* │ 3 │ singleCl.http │ 326.17 │ 65.23 │ 61.32 │ 198.08 │
* │ 4 │ transaction.libsql │ 856.79 │ 171.36 │ 132.31 │ 595.24 │
* │ 5 │ bulk.libsql │ 180.63 │ 36.13 │ 35.39 │ 107.71 │
* │ 6 │ singleKy.libsql │ 347.11 │ 69.42 │ 65.00 │ 207.14 │
* │ 7 │ singleCl.libsql │ 328.60 │ 65.72 │ 62.19 │ 195.04 │
* └───┴────────────────────┴────────┴────────┴────────┴────────┘
*/
}
});
test("count & exists", async () => {
const items = new Entity("items", [new TextField("label")]);
const em = new EntityManager([items], dummyConnection);
@@ -160,25 +46,44 @@ describe("[Repository]", async () => {
// count all
const res = await em.repository(items).count();
expect(res.sql).toBe('select count(*) as "count" from "items"');
expect(res.count).toBe(3);
expect(res.data.count).toBe(3);
//
{
const res = await em.repository(items).findMany();
expect(res.count).toBeUndefined();
}
{
const res = await em
.repository(items, {
includeCounts: true,
})
.findMany();
expect(res.count).toBe(3);
}
// count filtered
const res2 = await em.repository(items).count({ label: { $in: ["a", "b"] } });
const res2 = await em
.repository(items, {
includeCounts: true,
})
.count({ label: { $in: ["a", "b"] } });
expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
expect(res2.parameters).toEqual(["a", "b"]);
expect(res2.count).toBe(2);
expect(res2.data.count).toBe(2);
// check exists
const res3 = await em.repository(items).exists({ label: "a" });
expect(res3.exists).toBe(true);
expect(res3.data.exists).toBe(true);
const res4 = await em.repository(items).exists({ label: "d" });
expect(res4.exists).toBe(false);
expect(res4.data.exists).toBe(false);
// for now, allow empty filter
const res5 = await em.repository(items).exists({});
expect(res5.exists).toBe(true);
expect(res5.data.exists).toBe(true);
});
test("option: silent", async () => {
@@ -191,6 +96,9 @@ describe("[Repository]", async () => {
// should throw because table doesn't exist
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
// should silently return empty result
em.repo("items", { silent: true })
.findMany({})
.then((r) => r.data);
expect(
em
.repo("items", { silent: true })
@@ -209,16 +117,16 @@ describe("[Repository]", async () => {
expect(
em
.repo("items")
.repo("items", { includeCounts: true })
.findMany({})
.then((r) => [r.meta.count, r.meta.total]),
.then((r) => [r.count, r.total]),
).resolves.toEqual([0, 0]);
expect(
em
.repo("items", { includeCounts: false })
.findMany({})
.then((r) => [r.meta.count, r.meta.total]),
.then((r) => [r.count, r.total]),
).resolves.toEqual([undefined, undefined]);
});
});

View File

@@ -38,14 +38,15 @@ export function getLocalLibsqlConnection() {
return { url: "http://127.0.0.1:8080" };
}
type ConsoleSeverity = "log" | "warn" | "error";
type ConsoleSeverity = "debug" | "log" | "warn" | "error";
const _oldConsoles = {
debug: console.debug,
log: console.log,
warn: console.warn,
error: console.error,
};
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) {
export function disableConsoleLog(severities: ConsoleSeverity[] = ["debug", "log", "warn"]) {
severities.forEach((severity) => {
console[severity] = () => null;
});