Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(server): use kysely #12857

Merged
merged 12 commits into from
Jan 9, 2025
Merged

refactor(server): use kysely #12857

merged 12 commits into from
Jan 9, 2025

Conversation

mertalev
Copy link
Contributor

@mertalev mertalev commented Sep 23, 2024

Description

This PR transitions the asset, search and view repositories from TypeORM to Kysely. The goals of this change include type safety—including for complex queries—improved performance, more transparent behavior and better code composability. Other repositories as well as dev tooling will be migrated in later PRs to limit the scope of this one.

Notes:

  • Almost every query is faster than before, some significantly so. However, there is a possible regression for queries using both file and tag relations, in the order of single-digit milliseconds for 100 assets. This seems to be because the plan for the subquery approach is more reliant on random access through indices. This is only measuring the queries themselves — the difference may or may not disappear after accounting for the additional postprocessing that TypeORM needs to do.
  • Most repository methods have the same signatures as before to ease the transition. There are some exceptions to this:
    • Some upsert/update methods have Kysely types as inputs since this was an easy change and made things simpler
    • I made changes to some queries that necessitated a different signature
  • Kysely considers ORM-like upsert helpers to be out of scope. Since this is a common need, I made a few type-safe helpers to make these queries more ergonomic.
  • There can be differences in JSON-ified and normal DB responses. bytea types are returned as hex-encoded strings in the former and as Buffer in the latter. Timestamps likewise end with +00:00 in JSON and .000Z without.
    • +00:00 is more accurate if anything, but the inconsistency is a bit annoying.
    • I'm not sure if it's worth parsing the hex strings into buffers and back into base64 strings. The payload is smaller this way, but the server spends more time to do this.

Testing

Over the course of working on this branch, I've tested almost everything the web app can do at one point or another. But there may be regressions since testing certain functionality or subtle issues that I haven't noticed, and I haven't tested it with the mobile app.

@danieldietzler
Copy link
Member

Will this fix #11868?

@mertalev
Copy link
Contributor Author

Will this fix #11868?

Yup.

server/src/entities/asset.entity.ts Show resolved Hide resolved
server/src/interfaces/asset.interface.ts Show resolved Hide resolved
server/src/interfaces/search.interface.ts Outdated Show resolved Hide resolved
server/src/repositories/database.repository.ts Outdated Show resolved Hide resolved
const duplicates = await this.assetRepository.getDuplicates(auth.user.id);
return duplicates.map(({ duplicateId, assets }) => ({
duplicateId,
assets: assets.map((asset) => mapAsset(asset, { auth })),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you remove stacking?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense for how duplicate detection works right now. It won't detect the whole stack as being a duplicate, just particular members of that stack if they're similar enough. The primary asset could be a duplicate, but others might not be and vice versa. This could be addressed in a separate PR, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not against removing it, but I think it does make sense for those types of decisions to be transparently made, which most likely means happening in a separate PR.

server/src/utils/events.ts Outdated Show resolved Hide resolved
@mertalev mertalev force-pushed the feat/kysely branch 4 times, most recently from 92b0e58 to 2df635f Compare December 10, 2024 21:41
@mertalev mertalev force-pushed the feat/kysely branch 8 times, most recently from c6ee910 to 030cab8 Compare December 19, 2024 18:16
@mertalev mertalev marked this pull request as ready for review December 19, 2024 18:28
@mmomjian
Copy link
Contributor

Nice! Any changes here that would affect people using external Postgres? I don’t see any new extensions etc.

@mertalev
Copy link
Contributor Author

Nice! Any changes here that would affect people using external Postgres? I don’t see any new extensions etc.

I don't think so. It's an internal change

Copy link
Contributor

@alextran1502 alextran1502 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some questions about the migration. What are the steps we need to do after this PR get merged? Would you like to lay out the TODO list for the team?

@@ -97,10 +97,19 @@ const mapStack = (entity: AssetEntity) => {
return {
id: entity.stack.id,
primaryAssetId: entity.stack.primaryAssetId,
assetCount: entity.stack.assetCount ?? entity.stack.assets.length,
assetCount: entity.stack.assetCount ?? entity.stack.assets.length + 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it need to append 1 here?

Copy link
Contributor Author

@mertalev mertalev Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from a while ago. I think it was because entity.stack.assetCount includes the primary asset, but entity.stack.assets.length does not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this related to the kysely change?

server/src/dtos/duplicate.dto.ts Show resolved Hide resolved
types: {
date: {
to: 1184,
from: [1082, 1114, 1184],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do these numbers mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're Postgres data type IDs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the content in this file get generated? Can we provide some developer documentation for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It uses kysely-codegen. I could have sworn I added an npm run command for this, but maybe it got lost in a rebase at some point. I can add docs for it.

Copy link
Member

@danieldietzler danieldietzler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not done yet, so just my state of today

@@ -97,10 +97,19 @@ const mapStack = (entity: AssetEntity) => {
return {
id: entity.stack.id,
primaryAssetId: entity.stack.primaryAssetId,
assetCount: entity.stack.assetCount ?? entity.stack.assets.length,
assetCount: entity.stack.assetCount ?? entity.stack.assets.length + 1,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was that just a random bug you found along the way?

server/src/repositories/asset.repository.ts Outdated Show resolved Hide resolved
server/src/repositories/asset.repository.ts Outdated Show resolved Hide resolved
server/src/repositories/asset.repository.ts Outdated Show resolved Hide resolved
@@ -19,7 +19,8 @@
"preserveWatchOutput": true,
"baseUrl": "./",
"jsx": "react",
"types": ["vitest/globals"]
"types": ["vitest/globals"],
"noErrorTruncation": true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Kysely types can be pretty complex, so this tells VS Code to show more of the type when you hover over something.

builder.andWhere(`exifInfo.${key} = :${key}`, { [key]: value });
}
export const UPSERT_COLUMNS = {} as { [T in keyof DB]: { [K in keyof DB[T]]: RawBuilder<unknown> } };
/** Any repository that upserts to a table using `mapUpsertColumns` should call this method in its constructor with that table. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me understand the purpose of this mechanism?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an upsert is done, every field that's going to be updated has to be part of the query as excluded.{field}. We precompute the excluded.* for a table so mapUpsertColumns can reference this instead of recomputing it on each upsert.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help giving an example to demonstrate the idea?

Copy link
Contributor Author

@mertalev mertalev Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have a table with id, path, type and perform an update on path and type, mapUpsertColumns will return an object { path: sql'excluded.${sql.ref('path')}', type: sql'excluded.${sql.ref('type')}' }. It will exclude id from that object because it's in the conflictKeys. If you updated only path, then type would not be in the object either.

@alextran1502
Copy link
Contributor

I just performed a functional testing run.

  • The Tag page doesn't render the assets correctly
  • The sync endpoint throws the error below
immich_server            | [Nest] 28  - 12/20/2024, 4:06:42 PM   ERROR [Api:ErrorInterceptor~2thpecne] Unknown error: TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
immich_server            | TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
immich_server            |     at extname (node:path:1463:5)
immich_server            |     at Object.lookup (/usr/src/app/src/utils/mime-types.ts:90:51)
immich_server            |     at mapAsset (/usr/src/app/src/dtos/asset-response.dto.ts:140:33)
immich_server            |     at <anonymous> (/usr/src/app/src/services/sync.service.ts:24:38)
immich_server            |     at Array.map (<anonymous>)
immich_server            |     at SyncService.getFullSync (/usr/src/app/src/services/sync.service.ts:24:19)
immich_server            |     at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

@alextran1502
Copy link
Contributor

Hi Mert, the latest update causes the web timeline to be unable to load

@mertalev
Copy link
Contributor Author

mertalev commented Jan 2, 2025

Hi Mert, the latest update causes the web timeline to be unable to load

I haven't been able to reproduce this.

@alextran1502
Copy link
Contributor

Hi Mert, the latest update causes the web timeline to be unable to load

I haven't been able to reproduce this.

I am running the latest update with make dev-update and this is what I see, same behavior for album, and in incognito

image

Copy link
Contributor

@jrasm91 jrasm91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mertalev mertalev merged commit 2e12c46 into main Jan 9, 2025
36 checks passed
@mertalev mertalev deleted the feat/kysely branch January 9, 2025 16:15
aminesebastian pushed a commit to suburban-digital/immich-sd that referenced this pull request Jan 12, 2025
yosit pushed a commit to yosit/immich that referenced this pull request Jan 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants