"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateLockedColumns = exports.tokenizeForSearch = exports.dummy = exports.isVideoStreamSessionPkConstraint = exports.isAssetChecksumConstraint = exports.VIDEO_STREAM_SESSION_PK_CONSTRAINT = exports.ASSET_CHECKSUM_CONSTRAINT = exports.removeUndefinedKeys = exports.unnest = exports.asVector = exports.anyUuid = exports.asUuid = exports.getKyselyConfig = void 0;
exports.withDefaultVisibility = withDefaultVisibility;
exports.withExif = withExif;
exports.withExifInner = withExifInner;
exports.withAudioStream = withAudioStream;
exports.withVideoStream = withVideoStream;
exports.withVideoFormat = withVideoFormat;
exports.withVideoPackets = withVideoPackets;
exports.withSmartSearch = withSmartSearch;
exports.withFaces = withFaces;
exports.withFiles = withFiles;
exports.withFilePath = withFilePath;
exports.withFacesAndPeople = withFacesAndPeople;
exports.hasPeople = hasPeople;
exports.inAlbums = inAlbums;
exports.hasTags = hasTags;
exports.withOwner = withOwner;
exports.withLibrary = withLibrary;
exports.withTags = withTags;
exports.truncatedDate = truncatedDate;
exports.withTagId = withTagId;
exports.withEdits = withEdits;
exports.searchAssetBuilder = searchAssetBuilder;
exports.vectorIndexQuery = vectorIndexQuery;
const sql_tools_1 = require("@immich/sql-tools");
const kysely_1 = require("kysely");
const kysely_postgres_js_1 = require("kysely-postgres-js");
const postgres_1 = require("kysely/helpers/postgres");
const database_1 = require("../database");
const enum_1 = require("../enum");
const getKyselyConfig = (connection) => {
    return {
        dialect: new kysely_postgres_js_1.PostgresJSDialect({
            postgres: (0, sql_tools_1.createPostgres)({
                connection,
                onNotice: (notice) => {
                    if (notice['severity'] !== 'NOTICE') {
                        console.warn('Postgres notice:', notice);
                    }
                },
            }),
        }),
        log(event) {
            if (event.level === 'error') {
                if ((0, exports.isAssetChecksumConstraint)(event.error)) {
                    return;
                }
                console.error('Query failed :', {
                    durationMs: event.queryDurationMillis,
                    error: event.error,
                    sql: event.query.sql,
                    params: event.query.parameters,
                });
            }
        },
    };
};
exports.getKyselyConfig = getKyselyConfig;
const asUuid = (id) => (0, kysely_1.sql) `${id}::uuid`;
exports.asUuid = asUuid;
const anyUuid = (ids) => (0, kysely_1.sql) `any(${`{${ids}}`}::uuid[])`;
exports.anyUuid = anyUuid;
const asVector = (embedding) => (0, kysely_1.sql) `${`[${embedding}]`}::vector`;
exports.asVector = asVector;
const unnest = (array) => (0, kysely_1.sql) `unnest(array[${kysely_1.sql.join(array)}]::text[])`;
exports.unnest = unnest;
const removeUndefinedKeys = (update, template) => {
    for (const key in update) {
        if (template[key] === undefined) {
            delete update[key];
        }
    }
    return update;
};
exports.removeUndefinedKeys = removeUndefinedKeys;
exports.ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
exports.VIDEO_STREAM_SESSION_PK_CONSTRAINT = 'video_stream_session_pkey';
const isAssetChecksumConstraint = (error) => error?.constraint_name === exports.ASSET_CHECKSUM_CONSTRAINT;
exports.isAssetChecksumConstraint = isAssetChecksumConstraint;
const isVideoStreamSessionPkConstraint = (error) => error?.constraint_name === exports.VIDEO_STREAM_SESSION_PK_CONSTRAINT;
exports.isVideoStreamSessionPkConstraint = isVideoStreamSessionPkConstraint;
function withDefaultVisibility(qb) {
    return qb.where('asset.visibility', 'in', [kysely_1.sql.lit(enum_1.AssetVisibility.Archive), kysely_1.sql.lit(enum_1.AssetVisibility.Timeline)]);
}
function withExif(qb) {
    return qb
        .leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
        .select((eb) => eb.fn
        .toJson(eb.table('asset_exif'))
        .$castTo()
        .as('exifInfo'));
}
function withExifInner(qb) {
    return qb
        .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
        .select((eb) => eb.fn.toJson(eb.table('asset_exif')).as('exifInfo'))
        .$narrowType();
}
exports.dummy = (0, kysely_1.sql) `(select 1)`.as('dummy');
function withAudioStream(eb) {
    return (0, postgres_1.jsonObjectFrom)(eb
        .selectFrom(exports.dummy)
        .select(['asset_audio.index', 'asset_audio.codecName', 'asset_audio.profile', 'asset_audio.bitrate'])
        .where('asset_audio.assetId', 'is not', kysely_1.sql.lit(null))
        .$castTo());
}
function withVideoStream(eb) {
    return (0, postgres_1.jsonObjectFrom)(eb
        .selectFrom(exports.dummy)
        .select((eb) => [
        'asset_video.index',
        'asset_video.codecName',
        'asset_video.profile',
        'asset_video.level',
        'asset_video.bitrate',
        'asset_exif.exifImageWidth as width',
        'asset_exif.exifImageHeight as height',
        'asset_video.pixelFormat',
        'asset_video.frameCount',
        'asset_exif.fps as frameRate',
        'asset_video.timeBase',
        eb
            .case()
            .when('asset_exif.orientation', '=', kysely_1.sql.lit(enum_1.ExifOrientation.Rotate90CW.toString()))
            .then(kysely_1.sql.lit(-90))
            .when('asset_exif.orientation', '=', kysely_1.sql.lit(enum_1.ExifOrientation.Rotate270CW.toString()))
            .then(kysely_1.sql.lit(90))
            .when('asset_exif.orientation', '=', kysely_1.sql.lit(enum_1.ExifOrientation.Rotate180.toString()))
            .then(kysely_1.sql.lit(180))
            .else(0)
            .end()
            .as('rotation'),
        'asset_video.colorPrimaries',
        'asset_video.colorMatrix',
        'asset_video.colorTransfer',
        'asset_video.dvProfile',
        'asset_video.dvLevel',
        'asset_video.dvBlSignalCompatibilityId',
    ])
        .where('asset_video.assetId', 'is not', kysely_1.sql.lit(null))).$castTo();
}
function withVideoFormat(eb) {
    return (0, postgres_1.jsonObjectFrom)(eb
        .selectFrom(exports.dummy)
        .select(['asset_video.formatName', 'asset_video.formatLongName', 'asset.duration', 'asset_video.bitrate'])
        .where('asset_video.assetId', 'is not', kysely_1.sql.lit(null))).$castTo();
}
function withVideoPackets(eb) {
    return (0, postgres_1.jsonObjectFrom)(eb
        .selectFrom(exports.dummy)
        .where('asset_keyframe.assetId', 'is not', kysely_1.sql.lit(null))
        .select([
        'asset_keyframe.pts as keyframePts',
        'asset_keyframe.accDuration as keyframeAccDuration',
        'asset_keyframe.ownDuration as keyframeOwnDuration',
        'asset_keyframe.totalDuration',
        'asset_keyframe.packetCount',
        'asset_keyframe.outputFrames',
    ])).$castTo();
}
function withSmartSearch(qb) {
    return qb
        .leftJoin('smart_search', 'asset.id', 'smart_search.assetId')
        .select((eb) => (0, postgres_1.jsonObjectFrom)(eb.table('smart_search')).as('smartSearch'));
}
function withFaces(eb, withHidden, withDeletedFace) {
    return (0, postgres_1.jsonArrayFrom)(eb
        .selectFrom('asset_face')
        .selectAll('asset_face')
        .whereRef('asset_face.assetId', '=', 'asset.id')
        .$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null))
        .$if(!withHidden, (qb) => qb.where('asset_face.isVisible', '=', true))).as('faces');
}
function withFiles(eb, type) {
    return (0, postgres_1.jsonArrayFrom)(eb
        .selectFrom('asset_file')
        .select(database_1.columns.assetFiles)
        .whereRef('asset_file.assetId', '=', 'asset.id')
        .$if(!!type, (qb) => qb.where('asset_file.type', '=', type))).as('files');
}
function withFilePath(eb, type, isEdited = false) {
    return eb
        .selectFrom('asset_file')
        .select('asset_file.path')
        .whereRef('asset_file.assetId', '=', 'asset.id')
        .where('asset_file.type', '=', kysely_1.sql.lit(type))
        .where('asset_file.isEdited', '=', kysely_1.sql.lit(isEdited));
}
function withFacesAndPeople(eb, withHidden, withDeletedFace) {
    return (0, postgres_1.jsonArrayFrom)(eb
        .selectFrom('asset_face')
        .leftJoinLateral((eb) => eb.selectFrom('person').selectAll('person').whereRef('asset_face.personId', '=', 'person.id').as('person'), (join) => join.onTrue())
        .selectAll('asset_face')
        .select((eb) => eb.table('person').$castTo().as('person'))
        .whereRef('asset_face.assetId', '=', 'asset.id')
        .$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null))
        .$if(!withHidden, (qb) => qb.where('asset_face.isVisible', 'is', true))).as('faces');
}
function hasPeople(qb, personIds) {
    return qb.innerJoin((eb) => eb
        .selectFrom('asset_face')
        .select('assetId')
        .where('personId', '=', (0, exports.anyUuid)(personIds))
        .where('deletedAt', 'is', null)
        .where('isVisible', 'is', true)
        .groupBy('assetId')
        .having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
        .as('has_people'), (join) => join.onRef('has_people.assetId', '=', 'asset.id'));
}
function inAlbums(qb, albumIds) {
    return qb.innerJoin((eb) => eb
        .selectFrom('album_asset')
        .select('assetId')
        .where('albumId', '=', (0, exports.anyUuid)(albumIds))
        .groupBy('assetId')
        .having((eb) => eb.fn.count('albumId').distinct(), '=', albumIds.length)
        .as('has_album'), (join) => join.onRef('has_album.assetId', '=', 'asset.id'));
}
function hasTags(qb, tagIds) {
    return qb.innerJoin((eb) => eb
        .selectFrom('tag_asset')
        .select('assetId')
        .innerJoin('tag_closure', 'tag_asset.tagId', 'tag_closure.id_descendant')
        .where('tag_closure.id_ancestor', '=', (0, exports.anyUuid)(tagIds))
        .groupBy('assetId')
        .having((eb) => eb.fn.count('tag_closure.id_ancestor').distinct(), '>=', tagIds.length)
        .as('has_tags'), (join) => join.onRef('has_tags.assetId', '=', 'asset.id'));
}
function withOwner(eb) {
    return (0, postgres_1.jsonObjectFrom)(eb.selectFrom('user').select(database_1.columns.user).whereRef('user.id', '=', 'asset.ownerId')).as('owner');
}
function withLibrary(eb) {
    return (0, postgres_1.jsonObjectFrom)(eb.selectFrom('library').selectAll('library').whereRef('library.id', '=', 'asset.libraryId')).as('library');
}
function withTags(eb) {
    return (0, postgres_1.jsonArrayFrom)(eb
        .selectFrom('tag')
        .select(database_1.columns.tag)
        .innerJoin('tag_asset', 'tag.id', 'tag_asset.tagId')
        .whereRef('asset.id', '=', 'tag_asset.assetId')).as('tags');
}
function truncatedDate(order = enum_1.AssetOrderBy.TakenAt, size) {
    return (0, kysely_1.sql) `date_trunc(${kysely_1.sql.lit(size ?? 'MONTH')}, ${kysely_1.sql.ref(order === enum_1.AssetOrderBy.CreatedAt ? 'asset.createdAt' : 'localDateTime')} AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`;
}
function withTagId(qb, tagId) {
    return qb.where((eb) => eb.exists(eb
        .selectFrom('tag_closure')
        .innerJoin('tag_asset', 'tag_asset.tagId', 'tag_closure.id_descendant')
        .whereRef('tag_asset.assetId', '=', 'asset.id')
        .where('tag_closure.id_ancestor', '=', tagId)));
}
const isCJK = (c) => (c >= 0x4e_00 && c <= 0x9f_ff) ||
    (c >= 0xac_00 && c <= 0xd7_af) ||
    (c >= 0x30_40 && c <= 0x30_9f) ||
    (c >= 0x30_a0 && c <= 0x30_ff) ||
    (c >= 0x34_00 && c <= 0x4d_bf);
const tokenizeForSearch = (text) => {
    const tokens = [];
    let i = 0;
    while (i < text.length) {
        const c = text.charCodeAt(i);
        if (c <= 32) {
            i++;
            continue;
        }
        const start = i;
        if (isCJK(c)) {
            while (i < text.length && isCJK(text.charCodeAt(i))) {
                i++;
            }
            if (i - start === 1) {
                tokens.push(text[start]);
            }
            else {
                for (let k = start; k < i - 1; k++) {
                    tokens.push(text[k] + text[k + 1]);
                }
            }
        }
        else {
            while (i < text.length && text.charCodeAt(i) > 32 && !isCJK(text.charCodeAt(i))) {
                i++;
            }
            tokens.push(text.slice(start, i));
        }
    }
    return tokens;
};
exports.tokenizeForSearch = tokenizeForSearch;
function withEdits(eb) {
    return (0, postgres_1.jsonArrayFrom)(eb
        .selectFrom('asset_edit')
        .select(['asset_edit.action', 'asset_edit.parameters'])
        .whereRef('asset_edit.assetId', '=', 'asset.id')).as('edits');
}
const joinDeduplicationPlugin = new kysely_1.DeduplicateJoinsPlugin();
function searchAssetBuilder(kysely, options) {
    options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
    return kysely
        .withPlugin(joinDeduplicationPlugin)
        .selectFrom('asset')
        .$if(!!options.visibility, (qb) => options.visibility === 'not-locked'
        ? qb.where('asset.visibility', '!=', enum_1.AssetVisibility.Locked)
        : qb.where('asset.visibility', '=', options.visibility))
        .$if(!!options.albumIds && options.albumIds.length > 0, (qb) => inAlbums(qb, options.albumIds))
        .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds))
        .$if(options.tagIds === null, (qb) => qb.where((eb) => eb.not(eb.exists((eb) => eb.selectFrom('tag_asset').whereRef('assetId', '=', 'asset.id')))))
        .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds))
        .$if(!!options.createdBefore, (qb) => qb.where('asset.createdAt', '<=', options.createdBefore))
        .$if(!!options.createdAfter, (qb) => qb.where('asset.createdAt', '>=', options.createdAfter))
        .$if(!!options.updatedBefore, (qb) => qb.where('asset.updatedAt', '<=', options.updatedBefore))
        .$if(!!options.updatedAfter, (qb) => qb.where('asset.updatedAt', '>=', options.updatedAfter))
        .$if(!!options.trashedBefore, (qb) => qb.where('asset.deletedAt', '<=', options.trashedBefore))
        .$if(!!options.trashedAfter, (qb) => qb.where('asset.deletedAt', '>=', options.trashedAfter))
        .$if(!!options.takenBefore, (qb) => qb.where('asset.fileCreatedAt', '<=', options.takenBefore))
        .$if(!!options.takenAfter, (qb) => qb.where('asset.fileCreatedAt', '>=', options.takenAfter))
        .$if(options.city !== undefined, (qb) => qb
        .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
        .where('asset_exif.city', options.city === null ? 'is' : '=', options.city))
        .$if(options.state !== undefined, (qb) => qb
        .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
        .where('asset_exif.state', options.state === null ? 'is' : '=', options.state))
        .$if(options.country !== undefined, (qb) => qb
        .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
        .where('asset_exif.country', options.country === null ? 'is' : '=', options.country))
        .$if(options.make !== undefined, (qb) => qb
        .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
        .where('asset_exif.make', options.make === null ? 'is' : '=', options.make))
        .$if(options.model !== undefined, (qb) => qb
        .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
        .where('asset_exif.model', options.model === null ? 'is' : '=', options.model))
        .$if(options.lensModel !== undefined, (qb) => qb
        .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
        .where('asset_exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel))
        .$if(options.rating !== undefined, (qb) => qb
        .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
        .where('asset_exif.rating', options.rating === null ? 'is' : '=', options.rating))
        .$if(!!options.checksum, (qb) => qb.where('asset.checksum', '=', options.checksum))
        .$if(!!options.id, (qb) => qb.where('asset.id', '=', (0, exports.asUuid)(options.id)))
        .$if(!!options.libraryId, (qb) => qb.where('asset.libraryId', '=', (0, exports.asUuid)(options.libraryId)))
        .$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', (0, exports.anyUuid)(options.userIds)))
        .$if(!!options.encodedVideoPath, (qb) => qb
        .innerJoin('asset_file', (join) => join
        .onRef('asset.id', '=', 'asset_file.assetId')
        .on('asset_file.type', '=', enum_1.AssetFileType.EncodedVideo)
        .on('asset_file.isEdited', '=', false))
        .where('asset_file.path', '=', options.encodedVideoPath))
        .$if(!!options.originalPath, (qb) => qb.where((0, kysely_1.sql) `f_unaccent(asset."originalPath")`, 'ilike', (0, kysely_1.sql) `'%' || f_unaccent(${options.originalPath}) || '%'`))
        .$if(!!options.originalFileName, (qb) => qb.where((0, kysely_1.sql) `f_unaccent(asset."originalFileName")`, 'ilike', (0, kysely_1.sql) `'%' || f_unaccent(${options.originalFileName}) || '%'`))
        .$if(!!options.description, (qb) => qb
        .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
        .where((0, kysely_1.sql) `f_unaccent(asset_exif.description)`, 'ilike', (0, kysely_1.sql) `'%' || f_unaccent(${options.description}) || '%'`))
        .$if(!!options.ocr, (qb) => qb
        .innerJoin('ocr_search', 'asset.id', 'ocr_search.assetId')
        .where(() => (0, kysely_1.sql) `f_unaccent(ocr_search.text) %>> f_unaccent(${(0, exports.tokenizeForSearch)(options.ocr).join(' ')})`))
        .$if(!!options.type, (qb) => qb.where('asset.type', '=', options.type))
        .$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite))
        .$if(options.isOffline !== undefined, (qb) => qb.where('asset.isOffline', '=', options.isOffline))
        .$if(options.isEncoded !== undefined, (qb) => qb.where((eb) => {
        const exists = eb.exists((eb) => eb
            .selectFrom('asset_file')
            .whereRef('assetId', '=', 'asset.id')
            .where('type', '=', enum_1.AssetFileType.EncodedVideo));
        return options.isEncoded ? exists : eb.not(exists);
    }))
        .$if(options.isMotion !== undefined, (qb) => qb.where('asset.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null))
        .$if(!!options.isNotInAlbum && (!options.albumIds || options.albumIds.length === 0), (qb) => qb.where((eb) => eb.not(eb.exists((eb) => eb.selectFrom('album_asset').whereRef('assetId', '=', 'asset.id')))))
        .$if(options.withStacked === false, (qb) => qb.where('asset.stackId', 'is', null))
        .$if(!!options.withExif, withExifInner)
        .$if(!!(options.withFaces || options.withPeople), (qb) => qb.select(withFacesAndPeople))
        .$if(!options.withDeleted, (qb) => qb.where('asset.deletedAt', 'is', null));
}
function vectorIndexQuery({ vectorExtension, table, indexName, lists }) {
    switch (vectorExtension) {
        case enum_1.DatabaseExtension.VectorChord: {
            return `
        CREATE INDEX IF NOT EXISTS ${indexName} ON ${table} USING vchordrq (embedding vector_cosine_ops) WITH (options = $$
        residual_quantization = false
        [build.internal]
        lists = [${lists ?? 1}]
        spherical_centroids = true
        build_threads = 4
        sampling_factor = 1024
        $$)`;
        }
        case enum_1.DatabaseExtension.Vector: {
            return `
        CREATE INDEX IF NOT EXISTS ${indexName} ON ${table}
        USING hnsw (embedding vector_cosine_ops)
        WITH (ef_construction = 300, m = 16)`;
        }
        default: {
            throw new Error(`Unsupported vector extension: '${vectorExtension}'`);
        }
    }
}
const updateLockedColumns = (exif) => {
    exif.lockedProperties = database_1.lockableProperties.filter((property) => property in exif);
    return exif;
};
exports.updateLockedColumns = updateLockedColumns;
//# sourceMappingURL=database.js.map