1

I'm building a script that apply all .sql files found in a specific folder. I want those script to be executed one by one and stop execution if one fails. My folder tree is this (and I call my script by npm run migrate-dev from root folder)

root/
    ├── db/
    │   ├── migrations/
    │   │   ├── 01.sql
    │   │   └── 02.sql
    │   ├── dbConfig.js
    │   └── migrate.js
    ├── package.json
    └── .env

The two files in /migrations can be for example

01.sql

USE gamehub;

02.sql

CREATE TABLE `user` (`id` CHAR (32) NOT NULL);

migrate.js

const sqlParams = require('./dbConfig');
const mysql = require('mysql');
const fs = require('fs');

const pool = mysql.createPool(sqlParams);

if (process.env.RUN_MODE) {
  console.log(`Running in dev mode`);
}
const migrate = () => {
  console.log('Running migrations');
  // get files list
  fs.readdir('./db/migrations', (err, files) => {
    if (err) {
      console.log('Error while reading files in .db/migrations');
      console.log(err);
      throw err;
    }
    // generate promises list
    const promises = files.map(
      (file) =>
        new Promise((resolve, reject) => {
          fs.readFile(`./db/migrations/${file}`, 'utf-8', (err, data) => {
            if (err) {
              console.log(`Error while reading script ${file}`);
              reject(err);
            }
            pool.query(data, (err, results) => {
              if (err) {
                console.log('Error while querying SQL');
                reject(err);
              }
              resolve();
            });
          });
        })
    );
    console.log(promises);

    promises.reduce(async (prev, promise, index) => {
      await prev;
      console.log(`Executing script #${index}: ${files[index]}`);
      promise.then(
        () => Promise.resolve(),
        (err) => {
          console.log(err);
          return;
        }
      );
    }, Promise.resolve());
  });
};
migrate();

Problems are two:

  1. This code is not ended, I must end it manually with CTRL+C;
  2. If an error occurs the execution is not stopped.

Output in case no error occurred:
> node -r dotenv/config ./db/migrate.js

Running in dev mode
Running migrations
[ Promise { <pending> }, Promise { <pending> } ]
Executing script #0: 01_selectGameHub.sql       
Executing script #1: 02_createUserTable.sql   

Output in case error occurred (for example if user table already exists):

> node -r dotenv/config ./db/migrate.js

Running in dev mode
Running migrations
[ Promise { <pending> }, Promise { <pending> } ]
Executing script #0: 01_selectGameHub.sql
Executing script #1: 02_createUserTable.sql
Error while querying SQL
Error: ER_PARSE_ERROR: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')' at line 3
    at Query.Sequence._packetToError
[....]
    
    at Pool.query ([....]\node_modules\mysql\lib\Pool.js:199:23)
    at [....]\db\migrate.js:28:18
    at FSReqCallback.readFileAfterClose [as oncomplete] (internal/fs/read_file_context.js:63:3) {
  code: 'ER_PARSE_ERROR',
  errno: 1064,
  sqlMessage: "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')' at line 3",
  sqlState: '42000',
  index: 0,
  sql: 'CREATE TABLE `user` (\n\t`id` CHAR (32) NOT NULL,\n    );\n    '
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Zeno Dalla Valle
  • 957
  • 5
  • 16
  • There are a couple issues. The first one is that you are running your promises in parallel, without waiting for the previous ones to fulfill. And then, I don't see any line where you close the connection pool (which is why the script does not stop by itself, you have DB connections still ready to take queries). From [the docs](https://www.npmjs.com/package/mysql): `pool.end(err => { /* all connections in the pool have ended */ });` – blex Apr 18 '21 at 13:55
  • I thought that the `await prev;` was enought to prevent the parallel promises run in reduce – Zeno Dalla Valle Apr 18 '21 at 14:07
  • That would work in a `for` loop, but not in a `reduce`, and especially when you started them all in the `map` before going into that reduce – blex Apr 18 '21 at 14:08
  • So what's the right way to get the job done? – Zeno Dalla Valle Apr 18 '21 at 14:19
  • @ZenoDallaValle The task is executed when you create the promise and call `pool.query`, not when you `await` it later. Do not use `files.map(…)` if you want these to happen sequentially. – Bergi Apr 18 '21 at 15:18

1 Answers1

2

The first thing I would do would be to break that huge block of code into smaller, easier to understand, functions. Your logic will become really clear:

const migrate = async () => {
  console.log('Running migrations');
  const files = await getFilesList('./db/migrations');
  for (let i of files) {
    console.log(`Executing script: ${file}`);
    const data = await readFile(`./db/migrations/${file}`);
    await runQuery(data);
  }
};

function getFilesList(dir) {
  return new Promise((resolve, reject) => {
    fs.readdir(dir, (err, files) => {
      if (err) {
        console.log('Error while reading files in ' + dir);
        console.log(err);
        return reject(err);
      }
      resolve(files);
    });
  });
}

function readFile(file) {
  return new Promise((resolve, reject) => {
    fs.readFile(file, 'utf-8', (err, data) => {
      if (err) {
        console.log(`Error while reading script ${file}`);
        return reject(err);
      }
      resolve(data);
    });
  });
}

function runQuery() {
  return new Promise((resolve, reject) => {
    pool.query(data, (err, results) => {
      if (err) {
        console.log('Error while querying SQL');
        return reject(err);
      }
      resolve();
    });
  });
}

// Don't forget to close the pool, when you're done, or an error occurred:
migrate().catch(e => console.log(e)).finally(() => pool.end());

You could even go further into the simplification by using util.promisify, which converts callback functions into promise functions, so that your code remains consistent with promises:

const { promisify } = require("util");

const migrate = async() => {
  console.log('Running migrations');
  const files = await getFilesList('./db/migrations');
  for (let file of files) {
    console.log(`Executing script: ${file}`);
    const data = await readFile(`./db/migrations/${file}`);
    await runQuery(data);
  }
};

function getFilesList(dir) {
  return promisify(fs.readdir)(dir)
    .catch(err => {
      console.log('Error while reading files in ' + dir);
      throw err;
    });
}

function readFile(file) {
  return promisify(fs.readFile)(file, 'utf-8')
    .catch(err => {
      console.log(`Error while reading script ${file}`);
      throw err;
    });
}

function runQuery() {
  return promisify(pool.query.bind(pool))(data)
    .catch(err => {
      console.log('Error while querying SQL');
      throw err;
    });
}

// Don't forget to close the pool, when you're done, or an error occurred:
migrate().catch(e => console.log(e)).finally(() => pool.end());
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
blex
  • 24,941
  • 5
  • 39
  • 72
  • `readdir/readFile` have `sync` counterparts, which might come in handy here. Also, `let file of files` instead of `let i in files` would be better. – georg Apr 18 '21 at 14:48
  • @georg Yes, I kept it async, but OP is free to use the sync counterparts. As for the `for..in`, I did it on purpose since the OP wants to print the index in the console, inside the loop. But the filename seems to already contain that information, indeed – blex Apr 18 '21 at 14:50
  • 1
    "*using a package like es6-promisify*" - why not just `util.promisify` that comes natively with nodejs? Also `fs.promises`… – Bergi Apr 18 '21 at 15:19
  • @blex [Don't use `for…in` enumerations on arrays](https://stackoverflow.com/q/500504/1048572), though! If you want to print indices, write `for (let i=0; i – Bergi Apr 18 '21 at 15:21
  • 1
    Alright @Bergi, I fixed both. I wasn't aware that a native version of `promisify` existed! – blex Apr 18 '21 at 15:25
  • @blex Why `promisify(pool.query.bind(pool))(data)` needed `.bind(pool)` where fs didn't? What does it do? – Zeno Dalla Valle Apr 18 '21 at 16:07
  • 1
    When you do `promisify(fs.readdir)`, there is no scope issue, because `fs` is a simple Object. But `pool` is a specific instance of a class, and not binding the context to it (making sure `this` refers to `pool`) will cause scope issues, because the `query` method needs to know which instance you want to use. When you simply call `pool.query(...)`, it's not an issue. But when you pass it as a callback to `promisify`, it loses its context – blex Apr 18 '21 at 16:14
  • 1
    @ZenoDallaValle This might help you understand: https://jsbin.com/lokamabiqi/edit?js,console – blex Apr 18 '21 at 16:20