drizzle-orm icon indicating copy to clipboard operation
drizzle-orm copied to clipboard

[BUG]: Can't create index concurrently due to transaction block

Open MatanYadaev opened this issue 2 years ago • 2 comments

What version of drizzle-orm are you using?

0.27.0

What version of drizzle-kit are you using?

0.19.3

Describe the Bug

Creating an index with the CONCURRENTLY option throws this error: CREATE INDEX CONCURRENTLY cannot run inside a transaction block

Expected behavior

There's should be an option to run a specific migration without a transaction.

Environment & setup

Postgres 15.3

MatanYadaev avatar Jul 04 '23 08:07 MatanYadaev

Agree, will add this option

AndriiSherman avatar Jul 21 '23 18:07 AndriiSherman

Was this implemented?

clovis1122 avatar May 23 '24 15:05 clovis1122

@AndriiSherman I'm also dealing with this bug now, any update on it? Any chance we can prioritize?

thedewpoint avatar Jul 01 '24 15:07 thedewpoint

Could the orm not be smart enough to move this step outside a transaction? That may be the better choice? Ran into this issue and had to manually apply

nklisch avatar Oct 27 '24 17:10 nklisch

The migration kit should probably just include the transaction statements in the migration files. So it can enter and exit it as-needed?

dakdevs avatar Nov 18 '24 03:11 dakdevs

Curious if anyone has a workaround for this? We have a table with over 2m rows and we keep getting deadlocks when dealing with index creations.

dakdevs avatar Nov 18 '24 03:11 dakdevs

@Vinlock did you find a workaround?

AntonioAngelino avatar Nov 27 '24 18:11 AntonioAngelino

@AndriiSherman I know that this work is being prioritized, but can you recommend any workaround? I'm dealing with a table with millions of records and the only option I can think of is to run this sql manually and then run the migration (which shouldn't run since it checks if the index exists first)

I'm trying to avoid this though

thedewpoint avatar Dec 12 '24 16:12 thedewpoint

cc @L-Mario564 since i saw you were taking a look at this as well

thedewpoint avatar Dec 12 '24 16:12 thedewpoint

@AndriiSherman I use a technique, where I use drizzle index generation to have a colocated, single-source-of-truth schema definition. I then have a script that extracts all CREATE INDEX ... lines from the drizzle-generated migrations to a separate custom sql file. To migrate, I use a custom migrate.ts script instead of relying on drizzle-kit. The migrate script will handle the drizzle-generated migration files (which at this point have no index definitions anymore), and then will execute the custom sql file with the indexes, ensuring that each CREATE INDEX... statement is executed as a separate transaction. Hope this helps.

schema.ts

export const profiles = pgTable(
  'profiles',
  {
    id: uuid(),
  },
  (table) => [index().on(table.id).concurrently()],
);

package.json

{
  ...
  "scripts": {
    "generate": "npx drizzle-kit generate && tsx scripts/extractIndexes.ts",
    "migrate": "cross-env DB_MIGRATING=true tsx ./migrate.ts"
  },
  ...
}

extractIndexes.ts

import path from 'path';
import fs from 'fs';

const migrationsPath = path.join(process.cwd(), 'migrations');
const customMigrationsPath = path.join(process.cwd(), 'customMigrations');
const indexesFile = path.join(customMigrationsPath, '0000_CUSTOM_indexes.sql');

const sqlFiles = fs
  .readdirSync(migrationsPath)
  .filter((file: string) => file.endsWith('.sql'))
  .map((file: string) => path.join(migrationsPath, file));

for (const sqlFile of sqlFiles) {
  const sqlData = fs.readFileSync(sqlFile, 'utf8');

  // Find all CREATE INDEX statements (case insensitive)
  const indexLines = sqlData
    .split('\n')
    .filter((line) => /^\s*create\s+index/i.test(line.trim()))
    .map((line) => line.trim());

  if (indexLines.length > 0) {
    // Append the found CREATE INDEX statements to the custom indexes file
    fs.appendFileSync(
      indexesFile,
      `\n\n--> statement-breakpoint\n${indexLines
        .map((line) => line.replace('--> statement-breakpoint', ''))
        .join('\n\n--> statement-breakpoint\n')}`,
      'utf8',
    );

    // Remove the CREATE INDEX lines from the original file
    const updatedSqlData = sqlData
      .split('\n')
      .filter((line) => !indexLines.includes(line.trim()))
      .join('\n');

    fs.writeFileSync(sqlFile, updatedSqlData, 'utf8');

    console.log(
      `Extracted ${indexLines.length} index(es) from ${path.basename(sqlFile)}`,
    );
  }
}

migrate.ts

/* eslint-disable no-await-in-loop */
import path from 'path';
import fs from 'fs';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import { sql } from 'drizzle-orm';
import config from './drizzle.config';
import { db } from '.';

if (!process.env.DB_MIGRATING) {
  throw new Error(
    'You must set DB_MIGRATING to "true" when running migrations',
  );
}

if (
  process.env.DATABASE_URL !==
  'postgresql://postgres:[email protected]:54322/postgres'
) {
  throw new Error('You are running migrations in production');
}

// First run regular drizzle migrations
await migrate(db, {
  migrationsFolder: './migrations',
  migrationsSchema: config.migrations?.schema,
  migrationsTable: config.migrations?.table,
});

// Then handle custom migrations that need to run outside transactions
const customMigrationsPath = path.join(process.cwd(), 'customMigrations');
const sqlFiles = fs
  .readdirSync(customMigrationsPath)
  .filter((file: string) => file.endsWith('.sql'))
  .map((file: string) => path.join(customMigrationsPath, file));

// Use the raw connection for executing CONCURRENT index creation
for (const sqlFile of sqlFiles) {
  const sqlData = fs.readFileSync(sqlFile, 'utf8');
  const statements = sqlData.split('--> statement-breakpoint');

  // Execute each statement separately without transaction
  for (const statement of statements) {
    const trimmedStatement = statement.trim();
    if (trimmedStatement) {
      await db.execute(sql.raw(trimmedStatement));
    }
  }
}

await db.$client.end();

hsab avatar Dec 12 '24 17:12 hsab

Another hack, to deal with drizzle auto adding BEGIN statements.

You can add COMMIT;--> statement-breakpoint before the CREATE INDEX CONCURRENTLY command

e.g.

COMMIT;--> statement-breakpoint
CREATE INDEX CONCURRENTLY "indexName" ON "tableName" ("columnName");

gaspar09 avatar Dec 16 '24 03:12 gaspar09

Another hack, to deal with drizzle auto adding BEGIN statements.

You can add COMMIT;--> statement-breakpoint before the CREATE INDEX CONCURRENTLY command

e.g.

COMMIT;--> statement-breakpoint
CREATE INDEX CONCURRENTLY "indexName" ON "tableName" ("columnName");

I'll give this a shot thank you

thedewpoint avatar Dec 16 '24 15:12 thedewpoint