1

Check out this super-simple node.js program:

var g = { a : 1, b : 2 }

function callBack (key, value) {
    console.log("Callback called with key: " + key + "\nAnd value: " + value) ;
}

function doNothing (key, value, cb) {
    true ;
    console.log(key + ": doing nothing") ;
    cb() ;
}

function doLoop () {
    for (k in g) {
        f = function () {
            callBack(k, g[k]) ;
        }
        doNothing(k, g[k], f) ;
    }
}

doLoop() ;

When run, it produces this output:

a: doing nothing
Callback called with key: a
And value: 1
b: doing nothing
Callback called with key: b
And value: 2

OK. That makes sense - each time the callback is called, it has the correct arguments.

Now look at this program:

var mysql = require('mysql') ;
var dbClient = undefined ;
var db_uri = "mysql://xxx:xxx@127.0.0.1/xxx" ;

var schema = {
    redirects : "(id int AUTO_INCREMENT, key VARCHAR(50), url VARCHAR(2048))",
    clicks : "(ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP, IP VARBINARY(16))"
} ;

function createOnEmpty(err, results, fields, tableName, create_def) {
    console.log("createOnEmpty called on " + tableName) ;
    if (err) {
        console.error(err) ;
        process.exit(1) ;
    } else {
        if (0 == results.length) {
            dbClient.query(["create table ", tableName, create_def].join(" "),
                           function (err, results, fields) {} ) ;
        } else {
            console.log(tableName + " table already exists.") ;
        }
    }
    console.log("\n\n") ;
}

function setupSchema() {
    for (table in schema) {
        console.log("Checking for table: " + table) ;
        // FIXME: Why does this always seem to pass clicks as tablename?!
        dbClient.query(
           "show tables LIKE '" + table + "'",
           function (err, results, fields) {
               createOnEmpty(err, results, fields, table, schema[table])
           }
        );
    }
}

function handleDBConnect(err) {
    if (err) {
        console.error("ERROR: problem connecting to DB: " + err.code) ;
        process.exit(1) ;
    } else {
        console.log("Connected to database.") ;
        // Automatically set up the schema, if the tables don't exist
        setupSchema() ;
    }
}

function MySQLConnect() {
    dbClient = mysql.createConnection(db_uri) ;
    dbClient.connect(handleDBConnect) ;
}

MySQLConnect() ;

It outputs:

Connected to database.
Checking for table: redirects
Checking for table: clicks
createOnEmpty called on clicks



createOnEmpty called on clicks

The loop seems to be giving the argument 'clicks' as the argument 'table' both times, even though the variable has clearly been switched to 'redirects'.

I figure I must have some fundamental misunderstanding of how JavaScript/Node works here.

LJᛃ
  • 7,655
  • 2
  • 24
  • 35
Marco N.
  • 13
  • 3
  • Man I bet you if you change the wording of this question and add more code to help us make sense of this... you will probably get the answer you desire. – wuno Dec 27 '15 at 02:51

2 Answers2

2

To understand this behaviour you need to understand this 2 core js concepts:

  • Closures
  • Eventloop

Let say we have global variable a, log function which outputs a to console, and main function which calls log twice, first time with timeout (asynchronously), second time just simple function call

var a = 42;

function log() {
    console.log(`a is ${a}`);
}

function main() {
    setTimeout(log, 100);
    a = 13;
    log();
}

main();

This code produces the following output:

a is 13
a is 13

Why the hell the first time a is 13?

When you call setTimeout, it doesn't block main js thread for 100ms, it just adds log function to callback queue. The next line is a = 13. As a wasn't declared inside function body with var keyword, 13 assigned to a which was declared on the first line of code. Then we have the first line of output as a result of the last line of main function. Now we have empty callstack, nothing else happening in our code, but we still have log function in callback queue. After 100ms passed, if and only if callstack is empty (which is our case), log function could be called second time. It logs 'a is 13' once again, as a-s value was already reassigned.

This is a short explanation of how async callbacks work in javascript, and this is the reason why createOnEmpty called on clicks twice. dbClient.query is asynchronous and by the time it was called first time, your for loop finished its execution and table value is clicks. Quick and dirty solution of your problem will be

for (table in schema) {
    console.log("Checking for table: " + table) ;
    (function (table) {
        dbClient.query("show tables LIKE '" + table + "'",
                   function (err, results, fields) {
                       createOnEmpty(err, results, fields, table,
                                     schema[table]) } );
    )(table);
}

This immediately called function memorizes table value on each loop iteration in scope

Community
  • 1
  • 1
Andrei Lesnitsky
  • 1,038
  • 7
  • 14
  • Thanks, this was incredibly helpful. I knew there was something I didn't understand about how Javascript scope worked! – Marco N. Dec 27 '15 at 05:31
1

By the time the callback of dbClient.query is called the loop has already finished, leaving your (implicitly global) table variable at the last key in schema.

You need to scope it using an anonymous function(see the various answers there) or use a callback based iteration like so:

function setupSchema() {
    Object.keys(schema).forEach(function (table) {
        console.log("Checking for table: " + table) ;
        // FIXME: Why does this always seem to pass clicks as tablename?!
        dbClient.query(
           "show tables LIKE '" + table + "'",
           function (err, results, fields) {
               createOnEmpty(err, results, fields, table, schema[table])
           }
        );
    });
}
Community
  • 1
  • 1
LJᛃ
  • 7,655
  • 2
  • 24
  • 35