2

I have two files I'm working with here. query.php is a script I wrote to handle all MySQL queries for this particular website. query_test.js is a series of AJAX calls to test query.php. query_test.js, as you will see below, uses Promises to sequence one test after another, as I have made the tests dependent upon one another. By writing to the console, the AJAX calls appear to always be finishing up in the correct order. The problem is the output is inconsistent and the output of individual tests clearly are not always reflecting the results of the tests before them. Since individual tests seem to work perfectly fine, I strongly believe that something is a sequencing issue. I just can't figure out why and how this is happening.

For clarity, the tests are as follows:

  • Test 1: Retrieve entire table and display
  • Test 2: Add new row, then retrieve and display the table again
  • Test 3: Select the just-added row, display results
  • Test 4: Update the just-added row, retrieve and display table
  • Test 5: Remove the just-added row, retrieve and display table

In the example output below, notice that the table retrieved after Test 2 does not reflect that a row was added and Test 3's query returns nothing. It isn't until Test 4 that the previously-added row seems to show up. Each execution yields a different output; sometimes completely normal, and other times really wrong like the above. The console output always shows that each call is in the order it's supposed to be. That makes me think something in the backend is going wrong, but I'm clueless as to what that is.

query.php

<?php
/** 
The purpose of this script is to query the database in a number of ways. 
This script should be called via AJAX. See below for supported actions and 
required parameters for each.

 -------- ACTIONS ($_POST["action"])-----------

1. GET_TABLE: retrieve an entire table
    - Parameters: 
        "table_name": [String] name of the table 
    - Returns: [JSON] the entire table

2. UPDATE_TABLE: update a row in a table
    - Parameters: 
        "table_name": [String] name of the table
        "queries": [array] a list of queries, like so: <column>[<relational_operator]<value> to find rows to update
        "values": [array] key=>value pairs for each column to be updated.

3. SELECT_TABLE: select specified columns from specified rows
    - Parameters:
        "table_name": [String] name of the table
        "queries": [array] a list of queries, like so: <column>[<relational_operator]<value>
        "columns": (optional) [array] a list of column names to be returned. default value is '*', or all columns
    - Returns: [JSON] the rows returned from the query

4. ADD_ROW: add a row to a table
    - Parameters:
        "table_name": [String] name of the table
        "values": [array] key=>value pairs of columns names and corresponding values for the new row

5. REMOVE_ROW: remove a row or rows from a table
    - Parameters:
        "table_name": [String] name of the table
        "queries": [array] a list of queries, like so: <column>[<relational_operator]<value>

**/

// Constants
// Actions
define("GET_TABLE", 100); 
define("UPDATE_TABLE", 101);
define("SELECT_TABLE", 102); 
define("ADD_ROW", 103);
define("REMOVE_ROW", 104);

$server = "localhost";
$username = "root";
$password = "password";
$db_name = "test";

$conn = new mysqli($server, $username, $password, $db_name);
if ($conn->connect_error) die("Connection failed: " . $conn->connect_error);

// Action must be set
checkPOST("action") or die("Error: POST variable 'action' must be setand not empty.");
$action = $_POST["action"];

// Table name must be given for all actions
checkPOST("table_name") or die("Error: POST variable 'table_name' must be set and not empty.");
$table = $_POST["table_name"];

// Generic error message
$param_err = "Error: ensure all required params are set and not empty.";

// See which action needs to be done
switch ($action) {
    case GET_TABLE:
        // Simply run the query
        $q = "SELECT * FROM $table";
        $result = $conn->query($q);
        $result or die("Query '" . $q . "' failed: " . $conn->error);
        if ($result->num_rows > 0) {
            $output = array();
            while ($row = $result->fetch_assoc()) {
                $output[] = $row;
            }
            echo json_encode($output);
        }
        break;
    case UPDATE_TABLE:
        // Check for additional required params
        (checkPOST("values") && checkPOST("queries")) 
            or die($param_err);
        $values = $_POST["values"];
        $queries = $_POST["queries"];

        $q = "UPDATE $table";

        // Add the values to be set to the query
        $q .= " SET ";
        addQuotesToStrings($values);
        addItemsToQuery($q, $values, true);

        // Add the WHERE clause at the end of the query
        $q .= " WHERE ";
        addItemsToQuery($q, $queries, false);

        // Now ready to send off the query to the db and report success or failure
        $conn->query($q) or die("Query '" . $q . "' failed: " . $conn->error);
        echo "Successfully updated " . $conn->affected_rows . " rows.";

        break;
    case SELECT_TABLE:
        // Check for additional required params
        checkPOST("queries") or die($param_err);
        $queries = $_POST["queries"];

        $q = "SELECT ";

        // Add columns if specified
        if (checkPOST("columns")) {
            $columns = $_POST["columns"];
            addItemsToQuery($q, $columns, false);
        }
        else $q .= "* "; // No columns specified. Select all

        // Add table name
        $q .= "FROM $table ";

        // Add queries
        $q .= "WHERE ";
        addItemsToQuery($q, $queries, false);

        // Now, send off query
        $result = $conn->query($q);
        $result or die("Query '" . $q . "' failed: " . $conn->error);
        if ($result->num_rows > 0) {
            $output = array();
            while ($row = $result->fetch_assoc()) {
                $output[] = $row;
            }
            echo json_encode($output);
        }

        break;
    case ADD_ROW:
        // Check for POST var "values"
        checkPOST("values") or die($param_err);
        $values = $_POST["values"];

        $q = "INSERT INTO $table";

        // First, add column names
        $q .= " (";
        addItemsToQuery($q, array_keys($values), false);
        $q .= ") ";

        // Add the values
        $q .= "VALUES (";
        addQuotesToStrings($values);
        addItemsToQuery($q, $values, false);
        $q .= ")";

        // Run the query
        $conn->query($q) or die("Query '" . $q . "' failed: " . $conn->error);
        echo "Query was successful.";

        break;
    case REMOVE_ROW:
        // Check for queries
        checkPOST("queries") or die($param_err);
        $queries = $_POST["queries"];

        $q = "DELETE FROM $table";

        // Add queries
        $q .= " WHERE "; 
        addItemsToQuery($q, $queries, false);

        // Run query
        $conn->query($q) or die("Query '" . $q . "' failed: " . $conn->error);
        echo "Query affected " . $conn->affected_rows . " rows.";

        break;
    default:
        die("Error: POST variable 'action' has an unknown value.");
}

/**
    Adds items from an array to an SQL query string
    Assumes a space is present before the last keyword of the existing query.

    @param  string  &$q     A reference to an SQL query string
    @param  array   $items  An array containing strings that need to be added to a query in a list format (e.g. item1,item2,item3)
    $param  boolean $pairs  A boolean that indicated whether the items are key=>value pairs or not
**/
function addItemsToQuery(&$q, $items, $pairs) {
    $first = true;
    foreach ($items as $name => $item) {
        if (!$first) $q .= ", ";
        else $first = false;
        $q .= $pairs ? $name . "=" . $item  : $item;
    }
}

/**
    Adds single quotes to each string in a array of items for the purpose of being added to a MySQL query

    @param  array   $values A reference to an array of items
**/
function addQuotesToStrings(&$values) {
    foreach ($values as &$value) {
        if (strcmp(gettype($value), "string") == 0) $value = "'" . $value . "'"; 
    }
    unset($value);
}

/**
    Simple helper function to check if a POST var is set and not empty

    @param  string  $name   The name of the POST variable
**/
function checkPOST($name) {
    return isset($_POST[$name]) && !empty($_POST[$name]);
}
?>


query_test.js

    // This script is a series of AJAX calls to test query.php

const GET_TABLE = 100;
const UPDATE_TABLE = 101;
const SELECT_TABLE = 102;
const ADD_ROW = 103;
const REMOVE_ROW = 104;

/**
    Runs an AJAX request to query.php and displays the result

    @param  {jQuery}    div             A jQuery object div to place results
    @param  {array}     params          An assoc. array of POST variables for the AJAX call
    @param  {boolean}   displayAsTable  Determines where the result will be displayed as a table or as it is

    @return {Promise}   A Promise object
**/
function runTest(div, params, displayAsTable) {
    return Promise.resolve($.post("../php/query.php", params, function(data) {
        // Display result
        if (displayAsTable) {
            let tableData = JSON.parse(data);
            let table = $('<table></table>');
            for (let rowKey in tableData) {
                let row = $('<tr></tr>');
                for (let colKey in tableData[rowKey]) {
                    let col = $('<td></td>');
                    col.html(tableData[rowKey][colKey]);
                    row.append(col);
                }
                table.append(row);
            }
            div.append(table);
        }
        else div.append($('<p>' + data + '</p>'));
    }));
}

var num = 0;
// Temporary function to test if Promise order holds up
function logOrder(test) {
    num++;
    console.log(test + " finished: " + num);
}

// ----------- Test 1: Retrieve table ----------------
let div = $('<div></div><br>');
let header = $('<h2>Test 1: Retrieve table</h2>');
div.append(header);
$('body').append(div);
let params = {action: GET_TABLE, table_name: "projects"};
var promise = runTest(div, params, true);

// ------------ Test 2: Add Row ----------------------
promise = promise.then(function(value) {
    logOrder("Test 1");

    let div = $('<div></div><br>');
    let header = $('<h2>Test 2: Add Row</h2>');
    div.append(header);
    $('body').append(div);
    let values = {title: "test_proj", pic: "none.jpg", brief: "testing", description: "this is a test"};
    let params = {action: ADD_ROW, values: values, table_name: "projects"};
    runTest(div, params, false);
}, function(error) {
    alert(error);
});

// Get table to ensure row has been added
promise = promise.then(function(value) {
    logOrder("Test 2");

    let div = $('<div></div><br>');
    $('body').append(div);
    let params = {action: GET_TABLE, table_name: "projects"};
    runTest(div, params, true);
}, function(error) {
    alert(error);
});

// -------------- Test 3: Select table -------------------
promise = promise.then(function(value) {
    logOrder("Test 2 Check");

    let div = $('<div></div><br>');
    let header = $('<h2>Test 3: Select Table</h2>');
    div.append(header);
    $('body').append(div);
    let queries = ["title='test_proj'"];
    let params = {action: SELECT_TABLE, table_name: "projects", queries: queries};
    runTest(div, params, true);
}, function(error) {
    alert(error);
});

// -------------- Test 4: Update table -------------------
promise = promise.then(function(value) {
    logOrder("Test 3");

    let div = $('<div></div><br>');
    let header = $('<h2>Test 4: Update Table</h2>');
    div.append(header);
    $('body').append(div);
    let queries = ["title='test_proj'"];
    let values = {brief: "This was updated", description: "This was also updated"};
    let params = {action: UPDATE_TABLE, table_name: "projects", queries: queries, values: values};
    runTest(div, params, false);
}, function(error) {
    alert(error);
});

// Get table to ensure table has been updated
promise = promise.then(function(value) {
    logOrder("Test 4");

    let div = $('<div></div><br>');
    $('body').append(div);
    let params = {action: GET_TABLE, table_name: "projects"};
    runTest(div, params, true);
}, function(error) {
    alert(error);
});

// -------------- Test 5: Remove Row(s) ------------------
promise = promise.then(function(value) {
    logOrder("Test 4 Check");

    let div = $('<div></div><br>');
    let header = $('<h2>Test 5: Remove Row(s)</h2>');
    div.append(header);
    $('body').append(div);
    let queries = ["title='test_proj'"];
    let params = {action: REMOVE_ROW, table_name: "projects", queries: queries};
    runTest(div, params, false);
}, function(error) {
    alert(error);
});

// Get table to ensure row has been removed
promise = promise.then(function(value) {
    logOrder("Test 5");

    let div = $('<div></div>');
    $('body').append(div);
    let params = {action: GET_TABLE, table_name: "projects"};
    runTest(div, params, true);
}, function(error) {
    alert(error);
}).then(function(value) {
    logOrder("Test 5 Check");
}, function(error) {
    alert(error);
});


Example output:

Test 1: Retrieve table

11  placeholder none.jpg    placeholder placeholder description

Test 2: Add Row

Query was successful.

11  placeholder none.jpg    placeholder placeholder description

Test 3: Select Table


Test 4: Update Table

Successfully updated 1 rows.

11  placeholder none.jpg    placeholder placeholder description
17  test_proj   none.jpg    This was updated    This was also updated

Test 5: Remove Row(s)

Query affected 1 rows.

11  placeholder none.jpg    placeholder placeholder description
Levi Payne
  • 327
  • 2
  • 10

2 Answers2

1

I figured out what I was doing wrong after looking at this.

I made the changes suggested by @DarkKnight, but that wasn't the issue, or at least it wasn't the only issue.

It turns out I just needed to return a promise inside every promise.then() success function. Originally, I wasn't doing this. So after the first test, each subsequent test was starting the query but then calling the next then() instead of waiting for the result since nothing was being done with the new promise.

Here is the new query_test.js:

// This script is a series of AJAX calls to test query.php

const GET_TABLE = 100;
const UPDATE_TABLE = 101;
const SELECT_TABLE = 102;
const ADD_ROW = 103;
const REMOVE_ROW = 104;

/**
    Runs an AJAX request to query.php and displays the result

    @param  {jQuery}    div             A jQuery object div to place results
    @param  {array}     params          An assoc. array of POST variables for the AJAX call
    @param  {boolean}   displayAsTable  Determines where the result will be displayed as a table or as it is

    @return {Promise}   A Promise object
**/
function runTest(div, params, displayAsTable) {
    return new Promise(function(resolve, reject) {
        $.post("../php/query.php", params, function(data) {
            // Display result
            if (displayAsTable) {
                let tableData = JSON.parse(data);
                let table = $('<table></table>');
                for (let rowKey in tableData) {
                    let row = $('<tr></tr>');
                    for (let colKey in tableData[rowKey]) {
                        let col = $('<td></td>');
                        col.html(tableData[rowKey][colKey]);
                        row.append(col);
                    }
                    table.append(row);
                }
                div.append(table);
            }
            else div.append($('<p>' + data + '</p>'));
        }).then(resolve, reject)
    });
}

var num = 0;
// Temporary function to test if Promise order holds up
function logOrder(test) {
    num++;
    console.log(test + " finished: " + num);
}

var promise = Promise.resolve();

// ----------- Test 1: Retrieve table ----------------
promise = promise.then(function(value) {
    let div = $('<div></div><br>');
    let header = $('<h2>Test 1: Retrieve table</h2>');
    div.append(header);
    $('body').append(div);
    let params = {action: GET_TABLE, table_name: "projects"};
    return runTest(div, params, true);
}, function(error) {
    alert(error);
});

// ------------ Test 2: Add Row ----------------------
promise = promise.then(function(value) {
    logOrder("Test 1");

    let div = $('<div></div><br>');
    let header = $('<h2>Test 2: Add Row</h2>');
    div.append(header);
    $('body').append(div);
    let values = {title: "test_proj", pic: "none.jpg", brief: "testing", description: "this is a test"};
    let params = {action: ADD_ROW, values: values, table_name: "projects"};
    return runTest(div, params, false);
}, function(error) {
    alert(error);
});

// Get table to ensure row has been added
promise = promise.then(function(value) {
    logOrder("Test 2");

    let div = $('<div></div><br>');
    $('body').append(div);
    let params = {action: GET_TABLE, table_name: "projects"};
    return runTest(div, params, true);
}, function(error) {
    alert(error);
});

// -------------- Test 3: Select table -------------------
promise = promise.then(function(value) {
    logOrder("Test 2 Check");

    let div = $('<div></div><br>');
    let header = $('<h2>Test 3: Select Table</h2>');
    div.append(header);
    $('body').append(div);
    let queries = ["title='test_proj'"];
    let params = {action: SELECT_TABLE, table_name: "projects", queries: queries};
    return runTest(div, params, true);
}, function(error) {
    alert(error);
});

// -------------- Test 4: Update table -------------------
promise = promise.then(function(value) {
    logOrder("Test 3");

    let div = $('<div></div><br>');
    let header = $('<h2>Test 4: Update Table</h2>');
    div.append(header);
    $('body').append(div);
    let queries = ["title='test_proj'"];
    let values = {brief: "This was updated", description: "This was also updated"};
    let params = {action: UPDATE_TABLE, table_name: "projects", queries: queries, values: values};
    return runTest(div, params, false);
}, function(error) {
    alert(error);
});

// Get table to ensure table has been updated
promise = promise.then(function(value) {
    logOrder("Test 4");

    let div = $('<div></div><br>');
    $('body').append(div);
    let params = {action: GET_TABLE, table_name: "projects"};
    return runTest(div, params, true);
}, function(error) {
    alert(error);
});

// -------------- Test 5: Remove Row(s) ------------------
promise = promise.then(function(value) {
    logOrder("Test 4 Check");

    let div = $('<div></div><br>');
    let header = $('<h2>Test 5: Remove Row(s)</h2>');
    div.append(header);
    $('body').append(div);
    let queries = ["title='test_proj'"];
    let params = {action: REMOVE_ROW, table_name: "projects", queries: queries};
    return runTest(div, params, false);
}, function(error) {
    alert(error);
});

// Get table to ensure row has been removed
promise = promise.then(function(value) {
    logOrder("Test 5");

    let div = $('<div></div>');
    $('body').append(div);
    let params = {action: GET_TABLE, table_name: "projects"};
    return runTest(div, params, true);
}, function(error) {
    alert(error);
});

promise.then(function(value) {
    logOrder("Test 5 Check");
}, function(error) {
    alert(error);
});
Community
  • 1
  • 1
Levi Payne
  • 327
  • 2
  • 10
  • The difference you see is due solely to adding those `return runTest(...);` statements. `runTest()` didn't need changing - it returned a promise before and after the mod. Even better would be simply to write `return $.post(...);`. The outer `new Promise(...)` is completely unnecessary. – Roamer-1888 Sep 14 '16 at 20:25
  • 1
    Thanks for clarifying that. By changing `runTest()` back to its original form, I can verify that you are correct. I'm sure returning the result of `$.post()` would work as well, but my intention was to use the native Javascript Promise object, rather than the jQuery deferred object. – Levi Payne Sep 14 '16 at 20:42
0

In runTest(), Promise.resolve creates a Promise resolves to a jQuery Deferred object immediately, so these AJAX calls actually start in parallel.

To make AJAX calls start in series, runTest() should return a Promise resolves to the result instead, by converting jQuery Deferred objects to promises:

function runTest(div, params, displayAsTable) {
    return new Promise(function (resolve, reject) {
        $.post("../php/query.php", params, function(data) {
            // Display result
        }).then(resolve, reject)
    })
}

or you can try jQuery 3.0, Deferred should work with Promise without conversion.

Community
  • 1
  • 1
DarkKnight
  • 5,651
  • 2
  • 24
  • 36