import { PGlite } from '@electric-sql/pglite'; import { Client } from '@libsql/client/.'; import { Database } from 'better-sqlite3'; import { is } from 'drizzle-orm'; import { MySqlSchema, MySqlTable, MySqlView } from 'drizzle-orm/mysql-core'; import { getMaterializedViewConfig, isPgEnum, isPgMaterializedView, isPgSequence, isPgView, PgEnum, PgEnumObject, PgMaterializedView, PgPolicy, PgRole, PgSchema, PgSequence, PgTable, PgView, } from 'drizzle-orm/pg-core'; import { SingleStoreSchema, SingleStoreTable } from 'drizzle-orm/singlestore-core'; import { SQLiteTable, SQLiteView } from 'drizzle-orm/sqlite-core'; import * as fs from 'fs'; import { type Client as GelClient } from 'gel'; import { Connection } from 'mysql2/promise'; import { libSqlLogSuggestionsAndReturn } from 'src/cli/commands/libSqlPushUtils'; import { columnsResolver, enumsResolver, indPolicyResolver, mySqlViewsResolver, Named, policyResolver, roleResolver, schemasResolver, sequencesResolver, sqliteViewsResolver, tablesResolver, viewsResolver, } from 'src/cli/commands/migrate'; import { pgSuggestions } from 'src/cli/commands/pgPushUtils'; import { logSuggestionsAndReturn as singleStoreLogSuggestionsAndReturn } from 'src/cli/commands/singlestorePushUtils'; import { logSuggestionsAndReturn } from 'src/cli/commands/sqlitePushUtils'; import { Entities } from 'src/cli/validations/cli'; import { CasingType } from 'src/cli/validations/common'; import { schemaToTypeScript as schemaToTypeScriptGel } from 'src/introspect-gel'; import { schemaToTypeScript as schemaToTypeScriptMySQL } from 'src/introspect-mysql'; import { schemaToTypeScript } from 'src/introspect-pg'; import { schemaToTypeScript as schemaToTypeScriptSingleStore } from 'src/introspect-singlestore'; import { schemaToTypeScript as schemaToTypeScriptSQLite } from 'src/introspect-sqlite'; import { fromDatabase as fromGelDatabase } from 'src/serializer/gelSerializer'; import { prepareFromMySqlImports } from 'src/serializer/mysqlImports'; import { mysqlSchema, squashMysqlScheme, ViewSquashed } from 'src/serializer/mysqlSchema'; import { fromDatabase as fromMySqlDatabase, generateMySqlSnapshot } from 'src/serializer/mysqlSerializer'; import { prepareFromPgImports } from 'src/serializer/pgImports'; import { pgSchema, Policy, Role, squashPgScheme, View } from 'src/serializer/pgSchema'; import { fromDatabase, generatePgSnapshot } from 'src/serializer/pgSerializer'; import { prepareFromSingleStoreImports } from 'src/serializer/singlestoreImports'; import { singlestoreSchema, squashSingleStoreScheme } from 'src/serializer/singlestoreSchema'; import { fromDatabase as fromSingleStoreDatabase, generateSingleStoreSnapshot, } from 'src/serializer/singlestoreSerializer'; import { prepareFromSqliteImports } from 'src/serializer/sqliteImports'; import { sqliteSchema, squashSqliteScheme, View as SqliteView } from 'src/serializer/sqliteSchema'; import { fromDatabase as fromSqliteDatabase, generateSqliteSnapshot } from 'src/serializer/sqliteSerializer'; import { applyLibSQLSnapshotsDiff, applyMysqlSnapshotsDiff, applyPgSnapshotsDiff, applySingleStoreSnapshotsDiff, applySqliteSnapshotsDiff, Column, ColumnsResolverInput, ColumnsResolverOutput, Enum, PolicyResolverInput, PolicyResolverOutput, ResolverInput, ResolverOutput, ResolverOutputWithMoved, RolesResolverInput, RolesResolverOutput, Sequence, Table, TablePolicyResolverInput, TablePolicyResolverOutput, } from 'src/snapshotsDiffer'; export type PostgresSchema = Record< string, | PgTable | PgEnum | PgEnumObject | PgSchema | PgSequence | PgView | PgMaterializedView | PgRole | PgPolicy >; export type MysqlSchema = Record< string, MySqlTable | MySqlSchema | MySqlView >; export type SqliteSchema = Record | SQLiteView>; export type SinglestoreSchema = Record< string, SingleStoreTable | SingleStoreSchema /* | SingleStoreView */ >; export const testSchemasResolver = (renames: Set) => async (input: ResolverInput): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { created: input.created, renamed: [], deleted: input.deleted, }; } let createdSchemas = [...input.created]; let deletedSchemas = [...input.deleted]; const result: { created: Named[]; renamed: { from: Named; to: Named }[]; deleted: Named[]; } = { created: [], renamed: [], deleted: [] }; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedSchemas.findIndex((it) => { return it.name === from; }); if (idxFrom >= 0) { const idxTo = createdSchemas.findIndex((it) => { return it.name === to; }); result.renamed.push({ from: deletedSchemas[idxFrom], to: createdSchemas[idxTo], }); delete createdSchemas[idxTo]; delete deletedSchemas[idxFrom]; createdSchemas = createdSchemas.filter(Boolean); deletedSchemas = deletedSchemas.filter(Boolean); } } result.created = createdSchemas; result.deleted = deletedSchemas; return result; } catch (e) { console.error(e); throw e; } }; export const testSequencesResolver = (renames: Set) => async ( input: ResolverInput, ): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { created: input.created, moved: [], renamed: [], deleted: input.deleted, }; } let createdSequences = [...input.created]; let deletedSequences = [...input.deleted]; const result: { created: Sequence[]; moved: { name: string; schemaFrom: string; schemaTo: string }[]; renamed: { from: Sequence; to: Sequence }[]; deleted: Sequence[]; } = { created: [], renamed: [], deleted: [], moved: [] }; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedSequences.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === from; }); if (idxFrom >= 0) { const idxTo = createdSequences.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === to; }); const tableFrom = deletedSequences[idxFrom]; const tableTo = createdSequences[idxFrom]; if (tableFrom.schema !== tableTo.schema) { result.moved.push({ name: tableFrom.name, schemaFrom: tableFrom.schema, schemaTo: tableTo.schema, }); } if (tableFrom.name !== tableTo.name) { result.renamed.push({ from: deletedSequences[idxFrom], to: createdSequences[idxTo], }); } delete createdSequences[idxTo]; delete deletedSequences[idxFrom]; createdSequences = createdSequences.filter(Boolean); deletedSequences = deletedSequences.filter(Boolean); } } result.created = createdSequences; result.deleted = deletedSequences; return result; } catch (e) { console.error(e); throw e; } }; export const testEnumsResolver = (renames: Set) => async ( input: ResolverInput, ): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { created: input.created, moved: [], renamed: [], deleted: input.deleted, }; } let createdEnums = [...input.created]; let deletedEnums = [...input.deleted]; const result: { created: Enum[]; moved: { name: string; schemaFrom: string; schemaTo: string }[]; renamed: { from: Enum; to: Enum }[]; deleted: Enum[]; } = { created: [], renamed: [], deleted: [], moved: [] }; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedEnums.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === from; }); if (idxFrom >= 0) { const idxTo = createdEnums.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === to; }); const tableFrom = deletedEnums[idxFrom]; const tableTo = createdEnums[idxFrom]; if (tableFrom.schema !== tableTo.schema) { result.moved.push({ name: tableFrom.name, schemaFrom: tableFrom.schema, schemaTo: tableTo.schema, }); } if (tableFrom.name !== tableTo.name) { result.renamed.push({ from: deletedEnums[idxFrom], to: createdEnums[idxTo], }); } delete createdEnums[idxTo]; delete deletedEnums[idxFrom]; createdEnums = createdEnums.filter(Boolean); deletedEnums = deletedEnums.filter(Boolean); } } result.created = createdEnums; result.deleted = deletedEnums; return result; } catch (e) { console.error(e); throw e; } }; export const testTablesResolver = (renames: Set) => async ( input: ResolverInput, ): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { created: input.created, moved: [], renamed: [], deleted: input.deleted, }; } let createdTables = [...input.created]; let deletedTables = [...input.deleted]; const result: { created: Table[]; moved: { name: string; schemaFrom: string; schemaTo: string }[]; renamed: { from: Table; to: Table }[]; deleted: Table[]; } = { created: [], renamed: [], deleted: [], moved: [] }; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedTables.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === from; }); if (idxFrom >= 0) { const idxTo = createdTables.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === to; }); const tableFrom = deletedTables[idxFrom]; const tableTo = createdTables[idxFrom]; if (tableFrom.schema !== tableTo.schema) { result.moved.push({ name: tableFrom.name, schemaFrom: tableFrom.schema, schemaTo: tableTo.schema, }); } if (tableFrom.name !== tableTo.name) { result.renamed.push({ from: deletedTables[idxFrom], to: createdTables[idxTo], }); } delete createdTables[idxTo]; delete deletedTables[idxFrom]; createdTables = createdTables.filter(Boolean); deletedTables = deletedTables.filter(Boolean); } } result.created = createdTables; result.deleted = deletedTables; return result; } catch (e) { console.error(e); throw e; } }; export const testColumnsResolver = (renames: Set) => async ( input: ColumnsResolverInput, ): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { tableName: input.tableName, schema: input.schema, created: input.created, renamed: [], deleted: input.deleted, }; } let createdColumns = [...input.created]; let deletedColumns = [...input.deleted]; const renamed: { from: Column; to: Column }[] = []; const schema = input.schema || 'public'; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedColumns.findIndex((it) => { return `${schema}.${input.tableName}.${it.name}` === from; }); if (idxFrom >= 0) { const idxTo = createdColumns.findIndex((it) => { return `${schema}.${input.tableName}.${it.name}` === to; }); renamed.push({ from: deletedColumns[idxFrom], to: createdColumns[idxTo], }); delete createdColumns[idxTo]; delete deletedColumns[idxFrom]; createdColumns = createdColumns.filter(Boolean); deletedColumns = deletedColumns.filter(Boolean); } } return { tableName: input.tableName, schema: input.schema, created: createdColumns, deleted: deletedColumns, renamed, }; } catch (e) { console.error(e); throw e; } }; export const testPolicyResolver = (renames: Set) => async ( input: TablePolicyResolverInput, ): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { tableName: input.tableName, schema: input.schema, created: input.created, renamed: [], deleted: input.deleted, }; } let createdPolicies = [...input.created]; let deletedPolicies = [...input.deleted]; const renamed: { from: Policy; to: Policy }[] = []; const schema = input.schema || 'public'; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedPolicies.findIndex((it) => { return `${schema}.${input.tableName}.${it.name}` === from; }); if (idxFrom >= 0) { const idxTo = createdPolicies.findIndex((it) => { return `${schema}.${input.tableName}.${it.name}` === to; }); renamed.push({ from: deletedPolicies[idxFrom], to: createdPolicies[idxTo], }); delete createdPolicies[idxTo]; delete deletedPolicies[idxFrom]; createdPolicies = createdPolicies.filter(Boolean); deletedPolicies = deletedPolicies.filter(Boolean); } } return { tableName: input.tableName, schema: input.schema, created: createdPolicies, deleted: deletedPolicies, renamed, }; } catch (e) { console.error(e); throw e; } }; export const testIndPolicyResolver = (renames: Set) => async ( input: PolicyResolverInput, ): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { created: input.created, renamed: [], deleted: input.deleted, }; } let createdPolicies = [...input.created]; let deletedPolicies = [...input.deleted]; const renamed: { from: Policy; to: Policy }[] = []; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedPolicies.findIndex((it) => { return `${it.on}.${it.name}` === from; }); if (idxFrom >= 0) { const idxTo = createdPolicies.findIndex((it) => { return `${it.on}.${it.name}` === to; }); renamed.push({ from: deletedPolicies[idxFrom], to: createdPolicies[idxTo], }); delete createdPolicies[idxTo]; delete deletedPolicies[idxFrom]; createdPolicies = createdPolicies.filter(Boolean); deletedPolicies = deletedPolicies.filter(Boolean); } } return { created: createdPolicies, deleted: deletedPolicies, renamed, }; } catch (e) { console.error(e); throw e; } }; export const testRolesResolver = (renames: Set) => async ( input: RolesResolverInput, ): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { created: input.created, renamed: [], deleted: input.deleted, }; } let createdPolicies = [...input.created]; let deletedPolicies = [...input.deleted]; const renamed: { from: Policy; to: Policy }[] = []; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedPolicies.findIndex((it) => { return `${it.name}` === from; }); if (idxFrom >= 0) { const idxTo = createdPolicies.findIndex((it) => { return `${it.name}` === to; }); renamed.push({ from: deletedPolicies[idxFrom], to: createdPolicies[idxTo], }); delete createdPolicies[idxTo]; delete deletedPolicies[idxFrom]; createdPolicies = createdPolicies.filter(Boolean); deletedPolicies = deletedPolicies.filter(Boolean); } } return { created: createdPolicies, deleted: deletedPolicies, renamed, }; } catch (e) { console.error(e); throw e; } }; export const testViewsResolver = (renames: Set) => async ( input: ResolverInput, ): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { created: input.created, moved: [], renamed: [], deleted: input.deleted, }; } let createdViews = [...input.created]; let deletedViews = [...input.deleted]; const result: { created: View[]; moved: { name: string; schemaFrom: string; schemaTo: string }[]; renamed: { from: View; to: View }[]; deleted: View[]; } = { created: [], renamed: [], deleted: [], moved: [] }; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedViews.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === from; }); if (idxFrom >= 0) { const idxTo = createdViews.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === to; }); const viewFrom = deletedViews[idxFrom]; const viewTo = createdViews[idxFrom]; if (viewFrom.schema !== viewTo.schema) { result.moved.push({ name: viewFrom.name, schemaFrom: viewFrom.schema, schemaTo: viewTo.schema, }); } if (viewFrom.name !== viewTo.name) { result.renamed.push({ from: deletedViews[idxFrom], to: createdViews[idxTo], }); } delete createdViews[idxTo]; delete deletedViews[idxFrom]; createdViews = createdViews.filter(Boolean); deletedViews = deletedViews.filter(Boolean); } } result.created = createdViews; result.deleted = deletedViews; return result; } catch (e) { console.error(e); throw e; } }; export const testViewsResolverMySql = (renames: Set) => async ( input: ResolverInput, ): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { created: input.created, moved: [], renamed: [], deleted: input.deleted, }; } let createdViews = [...input.created]; let deletedViews = [...input.deleted]; const result: { created: ViewSquashed[]; moved: { name: string; schemaFrom: string; schemaTo: string }[]; renamed: { from: ViewSquashed; to: ViewSquashed }[]; deleted: ViewSquashed[]; } = { created: [], renamed: [], deleted: [], moved: [] }; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedViews.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === from; }); if (idxFrom >= 0) { const idxTo = createdViews.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === to; }); const viewFrom = deletedViews[idxFrom]; const viewTo = createdViews[idxFrom]; if (viewFrom.schema !== viewTo.schema) { result.moved.push({ name: viewFrom.name, schemaFrom: viewFrom.schema, schemaTo: viewTo.schema, }); } if (viewFrom.name !== viewTo.name) { result.renamed.push({ from: deletedViews[idxFrom], to: createdViews[idxTo], }); } delete createdViews[idxTo]; delete deletedViews[idxFrom]; createdViews = createdViews.filter(Boolean); deletedViews = deletedViews.filter(Boolean); } } result.created = createdViews; result.deleted = deletedViews; return result; } catch (e) { console.error(e); throw e; } }; export const testViewsResolverSingleStore = (renames: Set) => async ( input: ResolverInput, ): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { created: input.created, moved: [], renamed: [], deleted: input.deleted, }; } let createdViews = [...input.created]; let deletedViews = [...input.deleted]; const result: { created: ViewSquashed[]; moved: { name: string; schemaFrom: string; schemaTo: string }[]; renamed: { from: ViewSquashed; to: ViewSquashed }[]; deleted: ViewSquashed[]; } = { created: [], renamed: [], deleted: [], moved: [] }; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedViews.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === from; }); if (idxFrom >= 0) { const idxTo = createdViews.findIndex((it) => { return `${it.schema || 'public'}.${it.name}` === to; }); const viewFrom = deletedViews[idxFrom]; const viewTo = createdViews[idxFrom]; if (viewFrom.schema !== viewTo.schema) { result.moved.push({ name: viewFrom.name, schemaFrom: viewFrom.schema, schemaTo: viewTo.schema, }); } if (viewFrom.name !== viewTo.name) { result.renamed.push({ from: deletedViews[idxFrom], to: createdViews[idxTo], }); } delete createdViews[idxTo]; delete deletedViews[idxFrom]; createdViews = createdViews.filter(Boolean); deletedViews = deletedViews.filter(Boolean); } } result.created = createdViews; result.deleted = deletedViews; return result; } catch (e) { console.error(e); throw e; } }; export const testViewsResolverSqlite = (renames: Set) => async ( input: ResolverInput, ): Promise> => { try { if ( input.created.length === 0 || input.deleted.length === 0 || renames.size === 0 ) { return { created: input.created, moved: [], renamed: [], deleted: input.deleted, }; } let createdViews = [...input.created]; let deletedViews = [...input.deleted]; const result: { created: SqliteView[]; moved: { name: string; schemaFrom: string; schemaTo: string }[]; renamed: { from: SqliteView; to: SqliteView }[]; deleted: SqliteView[]; } = { created: [], renamed: [], deleted: [], moved: [] }; for (let rename of renames) { const [from, to] = rename.split('->'); const idxFrom = deletedViews.findIndex((it) => { return it.name === from; }); if (idxFrom >= 0) { const idxTo = createdViews.findIndex((it) => { return it.name === to; }); const viewFrom = deletedViews[idxFrom]; const viewTo = createdViews[idxFrom]; if (viewFrom.name !== viewTo.name) { result.renamed.push({ from: deletedViews[idxFrom], to: createdViews[idxTo], }); } delete createdViews[idxTo]; delete deletedViews[idxFrom]; createdViews = createdViews.filter(Boolean); deletedViews = deletedViews.filter(Boolean); } } result.created = createdViews; result.deleted = deletedViews; return result; } catch (e) { console.error(e); throw e; } }; export const diffTestSchemasPush = async ( client: PGlite, left: PostgresSchema, right: PostgresSchema, renamesArr: string[], cli: boolean = false, schemas: string[] = ['public'], casing?: CasingType | undefined, entities?: Entities, sqlStatementsToRun: { before?: string[]; after?: string[]; runApply?: boolean; } = { before: [], after: [], runApply: true, }, ) => { const shouldRunApply = sqlStatementsToRun.runApply === undefined ? true : sqlStatementsToRun.runApply; for (const st of sqlStatementsToRun.before ?? []) { await client.query(st); } if (shouldRunApply) { const res = await applyPgDiffs(left, casing); for (const st of res.sqlStatements) { await client.query(st); } } for (const st of sqlStatementsToRun.after ?? []) { await client.query(st); } const materializedViewsForRefresh = Object.values(left).filter((it) => isPgMaterializedView(it) ) as PgMaterializedView[]; // refresh all mat views for (const view of materializedViewsForRefresh) { const viewConf = getMaterializedViewConfig(view); if (viewConf.isExisting) continue; await client.exec( `REFRESH MATERIALIZED VIEW "${viewConf.schema ?? 'public'}"."${viewConf.name}"${ viewConf.withNoData ? ' WITH NO DATA;' : ';' }`, ); } // do introspect into PgSchemaInternal const introspectedSchema = await fromDatabase( { query: async (query: string, values?: any[] | undefined) => { const res = await client.query(query, values); return res.rows as any[]; }, }, undefined, schemas, entities, ); const leftTables = Object.values(right).filter((it) => is(it, PgTable)) as PgTable[]; const leftSchemas = Object.values(right).filter((it) => is(it, PgSchema)) as PgSchema[]; const leftEnums = Object.values(right).filter((it) => isPgEnum(it)) as PgEnum[]; const leftSequences = Object.values(right).filter((it) => isPgSequence(it)) as PgSequence[]; const leftRoles = Object.values(right).filter((it) => is(it, PgRole)) as PgRole[]; const leftPolicies = Object.values(right).filter((it) => is(it, PgPolicy)) as PgPolicy[]; const leftViews = Object.values(right).filter((it) => isPgView(it)) as PgView[]; const leftMaterializedViews = Object.values(right).filter((it) => isPgMaterializedView(it)) as PgMaterializedView[]; const serialized2 = generatePgSnapshot( leftTables, leftEnums, leftSchemas, leftSequences, leftRoles, leftPolicies, leftViews, leftMaterializedViews, casing, ); const { version: v1, dialect: d1, ...rest1 } = introspectedSchema; const { version: v2, dialect: d2, ...rest2 } = serialized2; const sch1 = { version: '7', dialect: 'postgresql', id: '0', prevId: '0', ...rest1, } as const; const sch2 = { version: '7', dialect: 'postgresql', id: '0', prevId: '0', ...rest2, } as const; const sn1 = squashPgScheme(sch1, 'push'); const sn2 = squashPgScheme(sch2, 'push'); const validatedPrev = pgSchema.parse(sch1); const validatedCur = pgSchema.parse(sch2); const renames = new Set(renamesArr); if (!cli) { const { sqlStatements, statements } = await applyPgSnapshotsDiff( sn1, sn2, testSchemasResolver(renames), testEnumsResolver(renames), testSequencesResolver(renames), testPolicyResolver(renames), testIndPolicyResolver(renames), testRolesResolver(renames), testTablesResolver(renames), testColumnsResolver(renames), testViewsResolver(renames), validatedPrev, validatedCur, 'push', ); const { shouldAskForApprove, statementsToExecute, columnsToRemove, tablesToRemove, tablesToTruncate, infoToPrint, schemasToRemove, matViewsToRemove, } = await pgSuggestions( { query: async (sql: string, params: any[] = []) => { return (await client.query(sql, params)).rows as T[]; }, }, statements, ); return { sqlStatements: statementsToExecute, statements, shouldAskForApprove, columnsToRemove, tablesToRemove, tablesToTruncate, infoToPrint, schemasToRemove, matViewsToRemove, }; } else { const { sqlStatements, statements } = await applyPgSnapshotsDiff( sn1, sn2, schemasResolver, enumsResolver, sequencesResolver, policyResolver, indPolicyResolver, roleResolver, tablesResolver, columnsResolver, viewsResolver, validatedPrev, validatedCur, 'push', ); return { sqlStatements, statements }; } }; export const applyPgDiffs = async ( sn: PostgresSchema, casing: CasingType | undefined, ) => { const dryRun = { version: '7', dialect: 'postgresql', id: '0', prevId: '0', tables: {}, enums: {}, views: {}, schemas: {}, sequences: {}, policies: {}, roles: {}, _meta: { schemas: {}, tables: {}, columns: {}, }, } as const; const tables = Object.values(sn).filter((it) => is(it, PgTable)) as PgTable[]; const schemas = Object.values(sn).filter((it) => is(it, PgSchema)) as PgSchema[]; const enums = Object.values(sn).filter((it) => isPgEnum(it)) as PgEnum[]; const sequences = Object.values(sn).filter((it) => isPgSequence(it)) as PgSequence[]; const roles = Object.values(sn).filter((it) => is(it, PgRole)) as PgRole[]; const views = Object.values(sn).filter((it) => isPgView(it)) as PgView[]; const policies = Object.values(sn).filter((it) => is(it, PgPolicy)) as PgPolicy[]; const materializedViews = Object.values(sn).filter((it) => isPgMaterializedView(it)) as PgMaterializedView[]; const serialized1 = generatePgSnapshot( tables, enums, schemas, sequences, roles, policies, views, materializedViews, casing, ); const { version: v1, dialect: d1, ...rest1 } = serialized1; const sch1 = { version: '7', dialect: 'postgresql', id: '0', prevId: '0', ...rest1, } as const; const sn1 = squashPgScheme(sch1); const validatedPrev = pgSchema.parse(dryRun); const validatedCur = pgSchema.parse(sch1); const { sqlStatements, statements } = await applyPgSnapshotsDiff( dryRun, sn1, testSchemasResolver(new Set()), testEnumsResolver(new Set()), testSequencesResolver(new Set()), testPolicyResolver(new Set()), testIndPolicyResolver(new Set()), testRolesResolver(new Set()), testTablesResolver(new Set()), testColumnsResolver(new Set()), testViewsResolver(new Set()), validatedPrev, validatedCur, ); return { sqlStatements, statements }; }; export const diffTestSchemas = async ( left: PostgresSchema, right: PostgresSchema, renamesArr: string[], cli: boolean = false, casing?: CasingType | undefined, ) => { const leftTables = Object.values(left).filter((it) => is(it, PgTable)) as PgTable[]; const rightTables = Object.values(right).filter((it) => is(it, PgTable)) as PgTable[]; const leftSchemas = Object.values(left).filter((it) => is(it, PgSchema)) as PgSchema[]; const rightSchemas = Object.values(right).filter((it) => is(it, PgSchema)) as PgSchema[]; const leftEnums = Object.values(left).filter((it) => isPgEnum(it)) as PgEnum[]; const rightEnums = Object.values(right).filter((it) => isPgEnum(it)) as PgEnum[]; const leftSequences = Object.values(left).filter((it) => isPgSequence(it)) as PgSequence[]; const rightSequences = Object.values(right).filter((it) => isPgSequence(it)) as PgSequence[]; const leftRoles = Object.values(left).filter((it) => is(it, PgRole)) as PgRole[]; const rightRoles = Object.values(right).filter((it) => is(it, PgRole)) as PgRole[]; const leftPolicies = Object.values(left).filter((it) => is(it, PgPolicy)) as PgPolicy[]; const rightPolicies = Object.values(right).filter((it) => is(it, PgPolicy)) as PgPolicy[]; const leftViews = Object.values(left).filter((it) => isPgView(it)) as PgView[]; const rightViews = Object.values(right).filter((it) => isPgView(it)) as PgView[]; const leftMaterializedViews = Object.values(left).filter((it) => isPgMaterializedView(it)) as PgMaterializedView[]; const rightMaterializedViews = Object.values(right).filter((it) => isPgMaterializedView(it)) as PgMaterializedView[]; const serialized1 = generatePgSnapshot( leftTables, leftEnums, leftSchemas, leftSequences, leftRoles, leftPolicies, leftViews, leftMaterializedViews, casing, ); const serialized2 = generatePgSnapshot( rightTables, rightEnums, rightSchemas, rightSequences, rightRoles, rightPolicies, rightViews, rightMaterializedViews, casing, ); const { version: v1, dialect: d1, ...rest1 } = serialized1; const { version: v2, dialect: d2, ...rest2 } = serialized2; const sch1 = { version: '7', dialect: 'postgresql', id: '0', prevId: '0', ...rest1, } as const; const sch2 = { version: '7', dialect: 'postgresql', id: '0', prevId: '0', ...rest2, } as const; const sn1 = squashPgScheme(sch1); const sn2 = squashPgScheme(sch2); const validatedPrev = pgSchema.parse(sch1); const validatedCur = pgSchema.parse(sch2); const renames = new Set(renamesArr); if (!cli) { const { sqlStatements, statements } = await applyPgSnapshotsDiff( sn1, sn2, testSchemasResolver(renames), testEnumsResolver(renames), testSequencesResolver(renames), testPolicyResolver(renames), testIndPolicyResolver(renames), testRolesResolver(renames), testTablesResolver(renames), testColumnsResolver(renames), testViewsResolver(renames), validatedPrev, validatedCur, ); return { sqlStatements, statements }; } else { const { sqlStatements, statements } = await applyPgSnapshotsDiff( sn1, sn2, schemasResolver, enumsResolver, sequencesResolver, policyResolver, indPolicyResolver, roleResolver, tablesResolver, columnsResolver, viewsResolver, validatedPrev, validatedCur, ); return { sqlStatements, statements }; } }; export const diffTestSchemasPushMysql = async ( client: Connection, left: MysqlSchema, right: MysqlSchema, renamesArr: string[], schema: string, cli: boolean = false, casing?: CasingType | undefined, ) => { const { sqlStatements } = await applyMySqlDiffs(left, casing); for (const st of sqlStatements) { await client.query(st); } // do introspect into PgSchemaInternal const introspectedSchema = await fromMySqlDatabase( { query: async (sql: string, params?: any[]) => { const res = await client.execute(sql, params); return res[0] as any; }, }, schema, ); const leftTables = Object.values(right).filter((it) => is(it, MySqlTable)) as MySqlTable[]; const leftViews = Object.values(right).filter((it) => is(it, MySqlView)) as MySqlView[]; const serialized2 = generateMySqlSnapshot(leftTables, leftViews, casing); const { version: v1, dialect: d1, ...rest1 } = introspectedSchema; const { version: v2, dialect: d2, ...rest2 } = serialized2; const sch1 = { version: '5', dialect: 'mysql', id: '0', prevId: '0', ...rest1, } as const; const sch2 = { version: '5', dialect: 'mysql', id: '0', prevId: '0', ...rest2, } as const; const sn1 = squashMysqlScheme(sch1); const sn2 = squashMysqlScheme(sch2); const validatedPrev = mysqlSchema.parse(sch1); const validatedCur = mysqlSchema.parse(sch2); const renames = new Set(renamesArr); if (!cli) { const { sqlStatements, statements } = await applyMysqlSnapshotsDiff( sn1, sn2, testTablesResolver(renames), testColumnsResolver(renames), testViewsResolverMySql(renames), validatedPrev, validatedCur, 'push', ); return { sqlStatements, statements }; } else { const { sqlStatements, statements } = await applyMysqlSnapshotsDiff( sn1, sn2, tablesResolver, columnsResolver, mySqlViewsResolver, validatedPrev, validatedCur, 'push', ); return { sqlStatements, statements }; } }; export const applyMySqlDiffs = async ( sn: MysqlSchema, casing: CasingType | undefined, ) => { const dryRun = { version: '5', dialect: 'mysql', id: '0', prevId: '0', views: {}, tables: {}, enums: {}, schemas: {}, _meta: { schemas: {}, tables: {}, columns: {}, }, } as const; const tables = Object.values(sn).filter((it) => is(it, MySqlTable)) as MySqlTable[]; const views = Object.values(sn).filter((it) => is(it, MySqlView)) as MySqlView[]; const serialized1 = generateMySqlSnapshot(tables, views, casing); const { version: v1, dialect: d1, ...rest1 } = serialized1; const sch1 = { version: '5', dialect: 'mysql', id: '0', prevId: '0', ...rest1, } as const; const sn1 = squashMysqlScheme(sch1); const validatedPrev = mysqlSchema.parse(dryRun); const validatedCur = mysqlSchema.parse(sch1); const { sqlStatements, statements } = await applyMysqlSnapshotsDiff( dryRun, sn1, testTablesResolver(new Set()), testColumnsResolver(new Set()), testViewsResolverMySql(new Set()), validatedPrev, validatedCur, ); return { sqlStatements, statements }; }; export const diffTestSchemasMysql = async ( left: MysqlSchema, right: MysqlSchema, renamesArr: string[], cli: boolean = false, casing?: CasingType | undefined, ) => { const leftTables = Object.values(left).filter((it) => is(it, MySqlTable)) as MySqlTable[]; const leftViews = Object.values(left).filter((it) => is(it, MySqlView)) as MySqlView[]; const rightTables = Object.values(right).filter((it) => is(it, MySqlTable)) as MySqlTable[]; const rightViews = Object.values(right).filter((it) => is(it, MySqlView)) as MySqlView[]; const serialized1 = generateMySqlSnapshot(leftTables, leftViews, casing); const serialized2 = generateMySqlSnapshot(rightTables, rightViews, casing); const { version: v1, dialect: d1, ...rest1 } = serialized1; const { version: v2, dialect: d2, ...rest2 } = serialized2; const sch1 = { version: '5', dialect: 'mysql', id: '0', prevId: '0', ...rest1, } as const; const sch2 = { version: '5', dialect: 'mysql', id: '0', prevId: '0', ...rest2, } as const; const sn1 = squashMysqlScheme(sch1); const sn2 = squashMysqlScheme(sch2); const validatedPrev = mysqlSchema.parse(sch1); const validatedCur = mysqlSchema.parse(sch2); const renames = new Set(renamesArr); if (!cli) { const { sqlStatements, statements } = await applyMysqlSnapshotsDiff( sn1, sn2, testTablesResolver(renames), testColumnsResolver(renames), testViewsResolverMySql(renames), validatedPrev, validatedCur, ); return { sqlStatements, statements }; } const { sqlStatements, statements } = await applyMysqlSnapshotsDiff( sn1, sn2, tablesResolver, columnsResolver, mySqlViewsResolver, validatedPrev, validatedCur, ); return { sqlStatements, statements }; }; export const diffTestSchemasSingleStore = async ( left: SinglestoreSchema, right: SinglestoreSchema, renamesArr: string[], cli: boolean = false, casing?: CasingType | undefined, ) => { const leftTables = Object.values(left).filter((it) => is(it, SingleStoreTable)) as SingleStoreTable[]; /* const leftViews = Object.values(left).filter((it) => is(it, SingleStoreView)) as SingleStoreView[]; */ const rightTables = Object.values(right).filter((it) => is(it, SingleStoreTable)) as SingleStoreTable[]; /* const rightViews = Object.values(right).filter((it) => is(it, SingleStoreView)) as SingleStoreView[]; */ const serialized1 = generateSingleStoreSnapshot( leftTables, /* leftViews, */ casing, ); const serialized2 = generateSingleStoreSnapshot( rightTables, /* rightViews, */ casing, ); const { version: v1, dialect: d1, ...rest1 } = serialized1; const { version: v2, dialect: d2, ...rest2 } = serialized2; const sch1 = { version: '1', dialect: 'singlestore', id: '0', prevId: '0', ...rest1, } as const; const sch2 = { version: '1', dialect: 'singlestore', id: '0', prevId: '0', ...rest2, } as const; const sn1 = squashSingleStoreScheme(sch1); const sn2 = squashSingleStoreScheme(sch2); const validatedPrev = singlestoreSchema.parse(sch1); const validatedCur = singlestoreSchema.parse(sch2); const renames = new Set(renamesArr); if (!cli) { const { sqlStatements, statements } = await applySingleStoreSnapshotsDiff( sn1, sn2, testTablesResolver(renames), testColumnsResolver(renames), /* testViewsResolverSingleStore(renames), */ validatedPrev, validatedCur, ); return { sqlStatements, statements }; } const { sqlStatements, statements } = await applySingleStoreSnapshotsDiff( sn1, sn2, tablesResolver, columnsResolver, /* singleStoreViewsResolver, */ validatedPrev, validatedCur, ); return { sqlStatements, statements }; }; export const diffTestSchemasPushSingleStore = async ( client: Connection, left: SinglestoreSchema, right: SinglestoreSchema, renamesArr: string[], schema: string, cli: boolean = false, casing?: CasingType | undefined, sqlStatementsToRun: { before?: string[]; after?: string[]; runApply?: boolean; } = { before: [], after: [], runApply: true, }, ) => { const shouldRunApply = sqlStatementsToRun.runApply === undefined ? true : sqlStatementsToRun.runApply; for (const st of sqlStatementsToRun.before ?? []) { await client.query(st); } if (shouldRunApply) { const res = await applySingleStoreDiffs(left, casing); for (const st of res.sqlStatements) { await client.query(st); } } for (const st of sqlStatementsToRun.after ?? []) { await client.query(st); } // do introspect into PgSchemaInternal const introspectedSchema = await fromSingleStoreDatabase( { query: async (sql: string, params?: any[]) => { const res = await client.execute(sql, params); return res[0] as any; }, }, schema, ); const leftTables = Object.values(right).filter((it) => is(it, SingleStoreTable)) as SingleStoreTable[]; /* const leftViews = Object.values(right).filter((it) => is(it, SingleStoreView)) as SingleStoreView[]; */ const serialized2 = generateSingleStoreSnapshot( leftTables, /* leftViews, */ casing, ); const { version: v1, dialect: d1, ...rest1 } = introspectedSchema; const { version: v2, dialect: d2, ...rest2 } = serialized2; const sch1 = { version: '1', dialect: 'singlestore', id: '0', prevId: '0', ...rest1, } as const; const sch2 = { version: '1', dialect: 'singlestore', id: '0', prevId: '0', ...rest2, } as const; const sn1 = squashSingleStoreScheme(sch1); const sn2 = squashSingleStoreScheme(sch2); const validatedPrev = singlestoreSchema.parse(sch1); const validatedCur = singlestoreSchema.parse(sch2); const renames = new Set(renamesArr); if (!cli) { const { sqlStatements, statements } = await applySingleStoreSnapshotsDiff( sn1, sn2, testTablesResolver(renames), testColumnsResolver(renames), /* testViewsResolverSingleStore(renames), */ validatedPrev, validatedCur, 'push', ); const { statementsToExecute, columnsToRemove, infoToPrint, shouldAskForApprove, tablesToRemove, tablesToTruncate, } = await singleStoreLogSuggestionsAndReturn( { query: async (sql: string, params?: any[]) => { const res = await client.execute(sql, params); return res[0] as T[]; }, }, statements, sn1, sn2, ); return { sqlStatements: statementsToExecute, statements, columnsToRemove, infoToPrint, shouldAskForApprove, tablesToRemove, tablesToTruncate, }; } else { const { sqlStatements, statements } = await applySingleStoreSnapshotsDiff( sn1, sn2, tablesResolver, columnsResolver, /* singleStoreViewsResolver, */ validatedPrev, validatedCur, 'push', ); return { sqlStatements, statements }; } }; export const applySingleStoreDiffs = async ( sn: SinglestoreSchema, casing: CasingType | undefined, ) => { const dryRun = { version: '1', dialect: 'singlestore', id: '0', prevId: '0', tables: {}, views: {}, enums: {}, schemas: {}, _meta: { schemas: {}, tables: {}, columns: {}, }, } as const; const tables = Object.values(sn).filter((it) => is(it, SingleStoreTable)) as SingleStoreTable[]; /* const views = Object.values(sn).filter((it) => is(it, SingleStoreView)) as SingleStoreView[]; */ const serialized1 = generateSingleStoreSnapshot(tables, /* views, */ casing); const { version: v1, dialect: d1, ...rest1 } = serialized1; const sch1 = { version: '1', dialect: 'singlestore', id: '0', prevId: '0', ...rest1, } as const; const sn1 = squashSingleStoreScheme(sch1); const validatedPrev = singlestoreSchema.parse(dryRun); const validatedCur = singlestoreSchema.parse(sch1); const { sqlStatements, statements } = await applySingleStoreSnapshotsDiff( dryRun, sn1, testTablesResolver(new Set()), testColumnsResolver(new Set()), /* testViewsResolverSingleStore(new Set()), */ validatedPrev, validatedCur, ); return { sqlStatements, statements }; }; export const diffTestSchemasPushSqlite = async ( client: Database, left: SqliteSchema, right: SqliteSchema, renamesArr: string[], cli: boolean = false, seedStatements: string[] = [], casing?: CasingType | undefined, ) => { const { sqlStatements } = await applySqliteDiffs(left, 'push'); for (const st of sqlStatements) { client.exec(st); } for (const st of seedStatements) { client.exec(st); } // do introspect into PgSchemaInternal const introspectedSchema = await fromSqliteDatabase( { query: async (sql: string, params: any[] = []) => { return client.prepare(sql).bind(params).all() as T[]; }, run: async (query: string) => { client.prepare(query).run(); }, }, undefined, ); const rightTables = Object.values(right).filter((it) => is(it, SQLiteTable)) as SQLiteTable[]; const rightViews = Object.values(right).filter((it) => is(it, SQLiteView)) as SQLiteView[]; const serialized2 = generateSqliteSnapshot(rightTables, rightViews, casing); const { version: v1, dialect: d1, ...rest1 } = introspectedSchema; const { version: v2, dialect: d2, ...rest2 } = serialized2; const sch1 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest1, } as const; const sch2 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest2, } as const; const sn1 = squashSqliteScheme(sch1, 'push'); const sn2 = squashSqliteScheme(sch2, 'push'); const renames = new Set(renamesArr); if (!cli) { const { sqlStatements, statements, _meta } = await applySqliteSnapshotsDiff( sn1, sn2, testTablesResolver(renames), testColumnsResolver(renames), testViewsResolverSqlite(renames), sch1, sch2, 'push', ); const { statementsToExecute, columnsToRemove, infoToPrint, schemasToRemove, shouldAskForApprove, tablesToRemove, tablesToTruncate, } = await logSuggestionsAndReturn( { query: async (sql: string, params: any[] = []) => { return client.prepare(sql).bind(params).all() as T[]; }, run: async (query: string) => { client.prepare(query).run(); }, }, statements, sn1, sn2, _meta!, ); return { sqlStatements: statementsToExecute, statements, columnsToRemove, infoToPrint, schemasToRemove, shouldAskForApprove, tablesToRemove, tablesToTruncate, }; } else { const { sqlStatements, statements } = await applySqliteSnapshotsDiff( sn1, sn2, tablesResolver, columnsResolver, sqliteViewsResolver, sch1, sch2, 'push', ); return { sqlStatements, statements }; } }; export async function diffTestSchemasPushLibSQL( client: Client, left: SqliteSchema, right: SqliteSchema, renamesArr: string[], cli: boolean = false, seedStatements: string[] = [], casing?: CasingType | undefined, ) { const { sqlStatements } = await applyLibSQLDiffs(left, 'push'); for (const st of sqlStatements) { await client.execute(st); } for (const st of seedStatements) { await client.execute(st); } const introspectedSchema = await fromSqliteDatabase( { query: async (sql: string, params?: any[]) => { const res = await client.execute({ sql, args: params || [] }); return res.rows as T[]; }, run: async (query: string) => { await client.execute(query); }, }, undefined, ); const leftTables = Object.values(right).filter((it) => is(it, SQLiteTable)) as SQLiteTable[]; const leftViews = Object.values(right).filter((it) => is(it, SQLiteView)) as SQLiteView[]; const serialized2 = generateSqliteSnapshot(leftTables, leftViews, casing); const { version: v1, dialect: d1, ...rest1 } = introspectedSchema; const { version: v2, dialect: d2, ...rest2 } = serialized2; const sch1 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest1, } as const; const sch2 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest2, } as const; const sn1 = squashSqliteScheme(sch1, 'push'); const sn2 = squashSqliteScheme(sch2, 'push'); const renames = new Set(renamesArr); if (!cli) { const { sqlStatements, statements, _meta } = await applyLibSQLSnapshotsDiff( sn1, sn2, testTablesResolver(renames), testColumnsResolver(renames), testViewsResolverSqlite(renames), sch1, sch2, 'push', ); const { statementsToExecute, columnsToRemove, infoToPrint, shouldAskForApprove, tablesToRemove, tablesToTruncate, } = await libSqlLogSuggestionsAndReturn( { query: async (sql: string, params?: any[]) => { const res = await client.execute({ sql, args: params || [] }); return res.rows as T[]; }, run: async (query: string) => { await client.execute(query); }, }, statements, sn1, sn2, _meta!, ); return { sqlStatements: statementsToExecute, statements, columnsToRemove, infoToPrint, shouldAskForApprove, tablesToRemove, tablesToTruncate, }; } else { const { sqlStatements, statements } = await applyLibSQLSnapshotsDiff( sn1, sn2, tablesResolver, columnsResolver, sqliteViewsResolver, sch1, sch2, 'push', ); return { sqlStatements, statements }; } } export const applySqliteDiffs = async ( sn: SqliteSchema, action?: 'push' | undefined, casing?: CasingType | undefined, ) => { const dryRun = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', tables: {}, enums: {}, views: {}, schemas: {}, _meta: { schemas: {}, tables: {}, columns: {}, }, } as const; const tables = Object.values(sn).filter((it) => is(it, SQLiteTable)) as SQLiteTable[]; const views = Object.values(sn).filter((it) => is(it, SQLiteView)) as SQLiteView[]; const serialized1 = generateSqliteSnapshot(tables, views, casing); const { version: v1, dialect: d1, ...rest1 } = serialized1; const sch1 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest1, } as const; const sn1 = squashSqliteScheme(sch1, action); const { sqlStatements, statements } = await applySqliteSnapshotsDiff( dryRun, sn1, testTablesResolver(new Set()), testColumnsResolver(new Set()), testViewsResolverSqlite(new Set()), dryRun, sch1, action, ); return { sqlStatements, statements }; }; export const applyLibSQLDiffs = async ( sn: SqliteSchema, action?: 'push' | undefined, casing?: CasingType | undefined, ) => { const dryRun = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', tables: {}, views: {}, enums: {}, schemas: {}, _meta: { schemas: {}, tables: {}, columns: {}, }, } as const; const tables = Object.values(sn).filter((it) => is(it, SQLiteTable)) as SQLiteTable[]; const views = Object.values(sn).filter((it) => is(it, SQLiteView)) as SQLiteView[]; const serialized1 = generateSqliteSnapshot(tables, views, casing); const { version: v1, dialect: d1, ...rest1 } = serialized1; const sch1 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest1, } as const; const sn1 = squashSqliteScheme(sch1, action); const { sqlStatements, statements } = await applyLibSQLSnapshotsDiff( dryRun, sn1, testTablesResolver(new Set()), testColumnsResolver(new Set()), testViewsResolverSqlite(new Set()), dryRun, sch1, action, ); return { sqlStatements, statements }; }; export const diffTestSchemasSqlite = async ( left: SqliteSchema, right: SqliteSchema, renamesArr: string[], cli: boolean = false, casing?: CasingType | undefined, ) => { const leftTables = Object.values(left).filter((it) => is(it, SQLiteTable)) as SQLiteTable[]; const leftViews = Object.values(left).filter((it) => is(it, SQLiteView)) as SQLiteView[]; const rightTables = Object.values(right).filter((it) => is(it, SQLiteTable)) as SQLiteTable[]; const rightViews = Object.values(right).filter((it) => is(it, SQLiteView)) as SQLiteView[]; const serialized1 = generateSqliteSnapshot(leftTables, leftViews, casing); const serialized2 = generateSqliteSnapshot(rightTables, rightViews, casing); const { version: v1, dialect: d1, ...rest1 } = serialized1; const { version: v2, dialect: d2, ...rest2 } = serialized2; const sch1 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest1, } as const; const sch2 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest2, } as const; const sn1 = squashSqliteScheme(sch1); const sn2 = squashSqliteScheme(sch2); const renames = new Set(renamesArr); if (!cli) { const { sqlStatements, statements } = await applySqliteSnapshotsDiff( sn1, sn2, testTablesResolver(renames), testColumnsResolver(renames), testViewsResolverSqlite(renames), sch1, sch2, ); return { sqlStatements, statements }; } const { sqlStatements, statements } = await applySqliteSnapshotsDiff( sn1, sn2, tablesResolver, columnsResolver, sqliteViewsResolver, sch1, sch2, ); return { sqlStatements, statements }; }; export const diffTestSchemasLibSQL = async ( left: SqliteSchema, right: SqliteSchema, renamesArr: string[], cli: boolean = false, casing?: CasingType | undefined, ) => { const leftTables = Object.values(left).filter((it) => is(it, SQLiteTable)) as SQLiteTable[]; const leftViews = Object.values(left).filter((it) => is(it, SQLiteView)) as SQLiteView[]; const rightTables = Object.values(right).filter((it) => is(it, SQLiteTable)) as SQLiteTable[]; const rightViews = Object.values(right).filter((it) => is(it, SQLiteView)) as SQLiteView[]; const serialized1 = generateSqliteSnapshot(leftTables, leftViews, casing); const serialized2 = generateSqliteSnapshot(rightTables, rightViews, casing); const { version: v1, dialect: d1, ...rest1 } = serialized1; const { version: v2, dialect: d2, ...rest2 } = serialized2; const sch1 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest1, } as const; const sch2 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest2, } as const; const sn1 = squashSqliteScheme(sch1); const sn2 = squashSqliteScheme(sch2); const renames = new Set(renamesArr); if (!cli) { const { sqlStatements, statements } = await applyLibSQLSnapshotsDiff( sn1, sn2, testTablesResolver(renames), testColumnsResolver(renames), testViewsResolverSqlite(renames), sch1, sch2, ); return { sqlStatements, statements }; } const { sqlStatements, statements } = await applyLibSQLSnapshotsDiff( sn1, sn2, tablesResolver, columnsResolver, sqliteViewsResolver, sch1, sch2, ); return { sqlStatements, statements }; }; // --- Introspect to file helpers --- export const introspectPgToFile = async ( client: PGlite, initSchema: PostgresSchema, testName: string, schemas: string[] = ['public'], entities?: Entities, casing?: CasingType | undefined, ) => { // put in db const { sqlStatements } = await applyPgDiffs(initSchema, casing); for (const st of sqlStatements) { await client.query(st); } // introspect to schema const introspectedSchema = await fromDatabase( { query: async (query: string, values?: any[] | undefined) => { const res = await client.query(query, values); return res.rows as any[]; }, }, undefined, schemas, entities, ); const { version: initV, dialect: initD, ...initRest } = introspectedSchema; const initSch = { version: '7', dialect: 'postgresql', id: '0', prevId: '0', ...initRest, } as const; const initSn = squashPgScheme(initSch); const validatedCur = pgSchema.parse(initSch); // write to ts file const file = schemaToTypeScript(introspectedSchema, 'camel'); fs.writeFileSync(`tests/introspect/postgres/${testName}.ts`, file.file); // generate snapshot from ts file const response = await prepareFromPgImports([ `tests/introspect/postgres/${testName}.ts`, ]); const afterFileImports = generatePgSnapshot( response.tables, response.enums, response.schemas, response.sequences, response.roles, response.policies, response.views, response.matViews, casing, ); const { version: v2, dialect: d2, ...rest2 } = afterFileImports; const sch2 = { version: '7', dialect: 'postgresql', id: '0', prevId: '0', ...rest2, } as const; const sn2AfterIm = squashPgScheme(sch2); const validatedCurAfterImport = pgSchema.parse(sch2); const { sqlStatements: afterFileSqlStatements, statements: afterFileStatements, } = await applyPgSnapshotsDiff( initSn, sn2AfterIm, testSchemasResolver(new Set()), testEnumsResolver(new Set()), testSequencesResolver(new Set()), testPolicyResolver(new Set()), testIndPolicyResolver(new Set()), testRolesResolver(new Set()), testTablesResolver(new Set()), testColumnsResolver(new Set()), testViewsResolver(new Set()), validatedCur, validatedCurAfterImport, ); fs.rmSync(`tests/introspect/postgres/${testName}.ts`); return { sqlStatements: afterFileSqlStatements, statements: afterFileStatements, }; }; export const introspectGelToFile = async ( client: GelClient, testName: string, schemas: string[] = ['public'], entities?: Entities, casing?: CasingType | undefined, ) => { // introspect to schema const introspectedSchema = await fromGelDatabase( { query: async (query: string, values?: any[] | undefined) => { const res = await client.querySQL(query, values); return res as any[]; }, }, undefined, schemas, entities, ); // write to ts file const file = schemaToTypeScriptGel(introspectedSchema, 'camel'); const path = `tests/introspect/gel/${testName}.ts`; fs.writeFileSync(path, file.file); return path; }; export const introspectMySQLToFile = async ( client: Connection, initSchema: MysqlSchema, testName: string, schema: string, casing?: CasingType | undefined, ) => { // put in db const { sqlStatements } = await applyMySqlDiffs(initSchema, casing); for (const st of sqlStatements) { await client.query(st); } // introspect to schema const introspectedSchema = await fromMySqlDatabase( { query: async (sql: string, params?: any[] | undefined) => { const res = await client.execute(sql, params); return res[0] as any; }, }, schema, ); const { version: initV, dialect: initD, ...initRest } = introspectedSchema; const initSch = { version: '5', dialect: 'mysql', id: '0', prevId: '0', ...initRest, } as const; const initSn = squashMysqlScheme(initSch); const validatedCur = mysqlSchema.parse(initSch); const file = schemaToTypeScriptMySQL(introspectedSchema, 'camel'); fs.writeFileSync(`tests/introspect/mysql/${testName}.ts`, file.file); const response = await prepareFromMySqlImports([ `tests/introspect/mysql/${testName}.ts`, ]); const afterFileImports = generateMySqlSnapshot( response.tables, response.views, casing, ); const { version: v2, dialect: d2, ...rest2 } = afterFileImports; const sch2 = { version: '5', dialect: 'mysql', id: '0', prevId: '0', ...rest2, } as const; const sn2AfterIm = squashMysqlScheme(sch2); const validatedCurAfterImport = mysqlSchema.parse(sch2); const { sqlStatements: afterFileSqlStatements, statements: afterFileStatements, } = await applyMysqlSnapshotsDiff( sn2AfterIm, initSn, testTablesResolver(new Set()), testColumnsResolver(new Set()), testViewsResolverMySql(new Set()), validatedCurAfterImport, validatedCur, ); fs.rmSync(`tests/introspect/mysql/${testName}.ts`); return { sqlStatements: afterFileSqlStatements, statements: afterFileStatements, }; }; export const introspectSingleStoreToFile = async ( client: Connection, initSchema: SinglestoreSchema, testName: string, schema: string, casing?: CasingType | undefined, ) => { // put in db const { sqlStatements } = await applySingleStoreDiffs(initSchema, casing); for (const st of sqlStatements) { await client.query(st); } // introspect to schema const introspectedSchema = await fromSingleStoreDatabase( { query: async (sql: string, params?: any[] | undefined) => { const res = await client.execute(sql, params); return res[0] as any; }, }, schema, ); const file = schemaToTypeScriptSingleStore(introspectedSchema, 'camel'); fs.writeFileSync(`tests/introspect/singlestore/${testName}.ts`, file.file); const response = await prepareFromSingleStoreImports([ `tests/introspect/singlestore/${testName}.ts`, ]); const afterFileImports = generateSingleStoreSnapshot( response.tables, /* response.views, */ casing, ); const { version: v2, dialect: d2, ...rest2 } = afterFileImports; const sch2 = { version: '1', dialect: 'singlestore', id: '0', prevId: '0', ...rest2, } as const; const sn2AfterIm = squashSingleStoreScheme(sch2); const validatedCurAfterImport = singlestoreSchema.parse(sch2); const leftTables = Object.values(initSchema).filter((it) => is(it, SingleStoreTable)) as SingleStoreTable[]; const initSnapshot = generateSingleStoreSnapshot( leftTables, /* response.views, */ casing, ); const { version: initV, dialect: initD, ...initRest } = initSnapshot; const initSch = { version: '1', dialect: 'singlestore', id: '0', prevId: '0', ...initRest, } as const; const initSn = squashSingleStoreScheme(initSch); const validatedCur = singlestoreSchema.parse(initSch); const { sqlStatements: afterFileSqlStatements, statements: afterFileStatements, } = await applySingleStoreSnapshotsDiff( sn2AfterIm, initSn, testTablesResolver(new Set()), testColumnsResolver(new Set()), /* testViewsResolverSingleStore(new Set()), */ validatedCurAfterImport, validatedCur, ); fs.rmSync(`tests/introspect/singlestore/${testName}.ts`); return { sqlStatements: afterFileSqlStatements, statements: afterFileStatements, }; }; export const introspectSQLiteToFile = async ( client: Database, initSchema: SqliteSchema, testName: string, casing?: CasingType | undefined, ) => { // put in db const { sqlStatements } = await applySqliteDiffs(initSchema); for (const st of sqlStatements) { client.exec(st); } // introspect to schema const introspectedSchema = await fromSqliteDatabase( { query: async (sql: string, params: any[] = []) => { return client.prepare(sql).bind(params).all() as T[]; }, run: async (query: string) => { client.prepare(query).run(); }, }, undefined, ); const { version: initV, dialect: initD, ...initRest } = introspectedSchema; const initSch = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...initRest, } as const; const initSn = squashSqliteScheme(initSch); const validatedCur = sqliteSchema.parse(initSch); const file = schemaToTypeScriptSQLite(introspectedSchema, 'camel'); fs.writeFileSync(`tests/introspect/sqlite/${testName}.ts`, file.file); const response = await prepareFromSqliteImports([ `tests/introspect/sqlite/${testName}.ts`, ]); const afterFileImports = generateSqliteSnapshot( response.tables, response.views, casing, ); const { version: v2, dialect: d2, ...rest2 } = afterFileImports; const sch2 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest2, } as const; const sn2AfterIm = squashSqliteScheme(sch2); const validatedCurAfterImport = sqliteSchema.parse(sch2); const { sqlStatements: afterFileSqlStatements, statements: afterFileStatements, } = await applySqliteSnapshotsDiff( sn2AfterIm, initSn, testTablesResolver(new Set()), testColumnsResolver(new Set()), testViewsResolverSqlite(new Set()), validatedCurAfterImport, validatedCur, ); fs.rmSync(`tests/introspect/sqlite/${testName}.ts`); return { sqlStatements: afterFileSqlStatements, statements: afterFileStatements, }; }; export const introspectLibSQLToFile = async ( client: Client, initSchema: SqliteSchema, testName: string, casing?: CasingType | undefined, ) => { // put in db const { sqlStatements } = await applyLibSQLDiffs(initSchema); for (const st of sqlStatements) { client.execute(st); } // introspect to schema const introspectedSchema = await fromSqliteDatabase( { query: async (sql: string, params: any[] = []) => { return (await client.execute({ sql, args: params })).rows as T[]; }, run: async (query: string) => { client.execute(query); }, }, undefined, ); const { version: initV, dialect: initD, ...initRest } = introspectedSchema; const initSch = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...initRest, } as const; const initSn = squashSqliteScheme(initSch); const validatedCur = sqliteSchema.parse(initSch); const file = schemaToTypeScriptSQLite(introspectedSchema, 'camel'); fs.writeFileSync(`tests/introspect/libsql/${testName}.ts`, file.file); const response = await prepareFromSqliteImports([ `tests/introspect/libsql/${testName}.ts`, ]); const afterFileImports = generateSqliteSnapshot( response.tables, response.views, casing, ); const { version: v2, dialect: d2, ...rest2 } = afterFileImports; const sch2 = { version: '6', dialect: 'sqlite', id: '0', prevId: '0', ...rest2, } as const; const sn2AfterIm = squashSqliteScheme(sch2); const validatedCurAfterImport = sqliteSchema.parse(sch2); const { sqlStatements: afterFileSqlStatements, statements: afterFileStatements, } = await applyLibSQLSnapshotsDiff( sn2AfterIm, initSn, testTablesResolver(new Set()), testColumnsResolver(new Set()), testViewsResolverSqlite(new Set()), validatedCurAfterImport, validatedCur, ); fs.rmSync(`tests/introspect/libsql/${testName}.ts`); return { sqlStatements: afterFileSqlStatements, statements: afterFileStatements, }; };