/* -*- mode: java; c-basic-offset: 4; indent-tabs-mode: nil; -*-
 *  vim:expandtab:shiftwidth=4:tabstop=4:smarttab:
 *
 *  Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License, version 2.0,
 *  as published by the Free Software Foundation.
 *
 *  This program is also distributed with certain software (including
 *  but not limited to OpenSSL) that is licensed under separate terms,
 *  as designated in a particular file or component or in included license
 *  documentation.  The authors of MySQL hereby grant you an additional
 *  permission to link the program and your derivative works with the
 *  separately licensed software that they have included with MySQL.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License, version 2.0, for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package com.mysql.cluster.crund;

import java.util.Arrays;

//import com.mysql.cluster.ndbj.*;
import com.mysql.cluster.ndbj.NdbClusterConnection;
import com.mysql.cluster.ndbj.Ndb;
import com.mysql.cluster.ndbj.NdbDictionary;
import com.mysql.cluster.ndbj.NdbTable;
import com.mysql.cluster.ndbj.NdbColumn;
import com.mysql.cluster.ndbj.NdbIndex;
import com.mysql.cluster.ndbj.NdbError;
import com.mysql.cluster.ndbj.NdbApiException;
import com.mysql.cluster.ndbj.NdbTransaction;
//import com.mysql.cluster.ndbj.ExecType; // ndbj-0.7.0
import com.mysql.cluster.ndbj.NdbTransaction.ExecType; // ndbj-0.7.1
import com.mysql.cluster.ndbj.NdbOperation;
import com.mysql.cluster.ndbj.NdbOperation.LockMode;
//import com.mysql.cluster.ndbj.AbortOption; // ndbj-0.7.0
import com.mysql.cluster.ndbj.NdbOperation.AbortOption; // ndbj-0.7.1
import com.mysql.cluster.ndbj.NdbScanOperation;
import com.mysql.cluster.ndbj.NdbIndexScanOperation;
import com.mysql.cluster.ndbj.NdbIndexScanOperation.BoundType;
import com.mysql.cluster.ndbj.NdbResultSet;


/**
 * THE CRUND benchmark implementation against NDB/J aka NDB-Bindings.
 */
public class NdbjLoad extends NdbBase {

    // ----------------------------------------------------------------------
    // NDBJ resources
    // ----------------------------------------------------------------------

    // singleton object representing the NDB cluster (one per process)
    protected NdbClusterConnection mgmd;

    // object representing a connection to an NDB database
    protected Ndb ndb;

    // the benchmark's metadata shortcuts
    protected Model model;

    // object representing an NDB database transaction
    protected NdbTransaction tx;

    // ----------------------------------------------------------------------
    // NDBJ intializers/finalizers
    // ----------------------------------------------------------------------

    protected void initProperties() {
        super.initProperties();
        descr = "->ndbj->ndbapi(" + mgmdConnect + ")";
    }

    protected void initLoad() throws Exception {
        // XXX support generic load class
        //super.init();

        // load native library (better diagnostics doing it explicitely)
        out.println();
        loadSystemLibrary("ndbj");

        // instantiate NDB cluster singleton
        out.println();
        out.print("creating cluster conn...");
        out.flush();
        mgmd = NdbClusterConnection.create(mgmdConnect);
        assert mgmd != null;
        out.println("    [ok]");

        // connect to cluster management node (ndb_mgmd)
        out.print("connecting to mgmd ...");
        out.flush();
        final int retries = 0;        // retries (< 0 = indefinitely)
        final int delay = 0;          // seconds to wait after retry
        final boolean verbose = true; // print report of progess
        // 0 = success, 1 = recoverable error, -1 = non-recoverable error
        if (mgmd.connect(retries, delay, verbose) != 0) {
            final String msg = ("mgmd@" + mgmdConnect
                                + " was not ready within "
                                + (retries * delay) + "s.");
            out.println(msg);
            throw new RuntimeException("!!! " + msg);
        }
        out.println("          [ok: " + mgmdConnect + "]");
    }

    protected void closeLoad() throws Exception {
        out.println();
        out.print("closing mgmd connection ...");
        out.flush();
        if (mgmd != null)
            mgmd.close();
        mgmd = null;
        out.println("     [ok]");

        // XXX support generic load class
        //super.close();
    }

    // ----------------------------------------------------------------------
    // NDBJ operations
    // ----------------------------------------------------------------------

    // returns a string representation of an NdbError
    static protected String toStr(NdbError e) {
        return "NdbError[" + e.getCode() + "]: " + e.getMessage();
    }

    // holds shortcuts to the benchmark's schema information
    static protected class Model {
        public final NdbTable table_A;
        public final NdbTable table_B0;
        public final NdbColumn column_A_id;
        public final NdbColumn column_A_cint;
        public final NdbColumn column_A_clong;
        public final NdbColumn column_A_cfloat;
        public final NdbColumn column_A_cdouble;
        public final NdbColumn column_B0_id;
        public final NdbColumn column_B0_cint;
        public final NdbColumn column_B0_clong;
        public final NdbColumn column_B0_cfloat;
        public final NdbColumn column_B0_cdouble;
        public final NdbColumn column_B0_a_id;
        public final NdbColumn column_B0_cvarbinary_def;
        public final NdbColumn column_B0_cvarchar_def;
        public final NdbIndex idx_B0_a_id;
        public final int attr_id;
        public final int attr_cint;
        public final int attr_clong;
        public final int attr_cfloat;
        public final int attr_cdouble;
        public final int attr_B0_a_id;
        public final int attr_B0_cvarbinary_def;
        public final int attr_B0_cvarchar_def;

        // XXX need names due to broken
        // NdbOperation.getValue(int), NdbResultSet.getXXX(int)
        public final String name_id;
        public final String name_cint;
        public final String name_clong;
        public final String name_cfloat;
        public final String name_cdouble;
        public final String name_B0_a_id;
        public final String name_B0_cvarbinary_def;
        public final String name_B0_cvarchar_def;

        // initialize this instance from the dictionary
        public Model(Ndb ndb) throws NdbApiException {
            final NdbDictionary dict = ndb.getDictionary();

            // get columns of table A
            if ((table_A = dict.getTable("a")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_A_id = table_A.getColumn("id")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_A_cint = table_A.getColumn("cint")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_A_clong = table_A.getColumn("clong")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_A_cfloat = table_A.getColumn("cfloat")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_A_cdouble = table_A.getColumn("cdouble")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));

            // get columns of table B0
            if ((table_B0 = dict.getTable("b0")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_B0_id = table_B0.getColumn("id")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_B0_cint = table_B0.getColumn("cint")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_B0_clong = table_B0.getColumn("clong")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_B0_cfloat = table_B0.getColumn("cfloat")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_B0_cdouble = table_B0.getColumn("cdouble")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_B0_a_id = table_B0.getColumn("a_id")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_B0_cvarbinary_def = table_B0.getColumn("cvarbinary_def")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));
            if ((column_B0_cvarchar_def = table_B0.getColumn("cvarchar_def")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));

            // get indexes of table B0
            if ((idx_B0_a_id = dict.getIndex("I_B0_FK", "b0")) == null)
                throw new RuntimeException(toStr(dict.getNdbError()));

            // get common attribute ids for tables A, B0
            attr_id = column_A_id.getColumnNo();
            if (attr_id != column_B0_id.getColumnNo())
                throw new RuntimeException("attribute id mismatch");
            attr_cint = column_A_cint.getColumnNo();
            if (attr_cint != column_B0_cint.getColumnNo())
                throw new RuntimeException("attribute id mismatch");
            attr_clong = column_A_clong.getColumnNo();
            if (attr_clong != column_B0_clong.getColumnNo())
                throw new RuntimeException("attribute id mismatch");
            attr_cfloat = column_A_cfloat.getColumnNo();
            if (attr_cfloat != column_B0_cfloat.getColumnNo())
                throw new RuntimeException("attribute id mismatch");
            attr_cdouble = column_A_cdouble.getColumnNo();
            if (attr_cdouble != column_B0_cdouble.getColumnNo())
                throw new RuntimeException("attribute id mismatch");

            // get attribute ids for table B0
            attr_B0_a_id = column_B0_a_id.getColumnNo();
            attr_B0_cvarbinary_def = column_B0_cvarbinary_def.getColumnNo();
            attr_B0_cvarchar_def = column_B0_cvarchar_def.getColumnNo();

            // XXX need names due to broken
            // NdbOperation.getValue(int), NdbResultSet.getXXX(int)
            name_id = column_A_id.getName();
            if (!name_id.equals(column_B0_id.getName()))
                throw new RuntimeException("attribute name mismatch");
            name_cint = column_A_cint.getName();
            if (!name_cint.equals(column_B0_cint.getName()))
                throw new RuntimeException("attribute name mismatch");
            name_clong = column_A_clong.getName();
            if (!name_clong.equals(column_B0_clong.getName()))
                throw new RuntimeException("attribute name mismatch");
            name_cfloat = column_A_cfloat.getName();
            if (!name_cfloat.equals(column_B0_cfloat.getName()))
                throw new RuntimeException("attribute name mismatch");
            name_cdouble = column_A_cdouble.getName();
            if (!name_cdouble.equals(column_B0_cdouble.getName()))
                throw new RuntimeException("attribute name mismatch");
            name_B0_a_id = column_B0_a_id.getName();
            name_B0_cvarbinary_def = column_B0_cvarbinary_def.getName();
            name_B0_cvarchar_def = column_B0_cvarchar_def.getName();
        }
    };

    // some string and byte literals
    final String string1 = "i";
    final String string10 = "xxxxxxxxxx";
    final String string100 = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
    final String string1000 = "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm";
    final byte[] bytes1 = string1.getBytes();
    final byte[] bytes10 = string10.getBytes();
    final byte[] bytes100 = string100.getBytes();
    final byte[] bytes1000 = string1000.getBytes();
    final String[] strings = { string1, string10, string100 };
    final byte[][] bytes = { bytes1, bytes10, bytes100 };

    protected void initOperations() throws NdbApiException {
        out.print("initializing operations ...");
        out.flush();

        //out.println("default charset: "
        //    + java.nio.charset.Charset.defaultCharset().displayName());

        for (boolean f = false, done = false; !done; done = f, f = true) {
            // inner classes can only refer to a constant
            final boolean batch = f;
            final boolean forceSend = f;
            final boolean setAttrs = true;

            ops.add(
                new Op("insA" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        ins(model.table_A, 1, nOps, !setAttrs, batch);
                    }
                });

            ops.add(
                new Op("insB0" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        ins(model.table_B0, 1, nOps, !setAttrs, batch);
                    }
                });

            ops.add(
                new Op("setAByPK" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        setByPK(model.table_A, 1, nOps, batch);
                    }
                });

            ops.add(
                new Op("setB0ByPK" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        setByPK(model.table_B0, 1, nOps, batch);
                    }
                });

            ops.add(
                new Op("getAByPK" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        getByPK(model.table_A, 1, nOps, batch);
                    }
                });

            ops.add(
                new Op("getB0ByPK" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        getByPK(model.table_B0, 1, nOps, batch);
                    }
                });

            for (int i = 0, l = 1; l <= maxVarbinaryBytes; l *= 10, i++) {
                final byte[] b = bytes[i];
                assert l == b.length;

                ops.add(
                    new Op("setVarbinary" + l + (batch ? "_batch" : "")) {
                        public void run(int nOps)
                            throws NdbApiException {
                            setVarbinary(model.table_B0, 1, nOps, batch, b);
                        }
                    });

                ops.add(
                    new Op("getVarbinary" + l + (batch ? "_batch" : "")) {
                        public void run(int nOps)
                            throws NdbApiException {
                            getVarbinary(model.table_B0, 1, nOps, batch, b);
                        }
                    });
            }

            for (int i = 0, l = 1; l <= maxVarcharChars; l *= 10, i++) {
                final String s = strings[i];
                assert l == s.length();

                ops.add(
                    new Op("setVarchar" + l + (batch ? "_batch" : "")) {
                        public void run(int nOps)
                            throws NdbApiException {
                            setVarchar(model.table_B0, 1, nOps, batch, s);
                        }
                    });

                ops.add(
                    new Op("getVarchar" + l + (batch ? "_batch" : "")) {
                        public void run(int nOps)
                            throws NdbApiException {
                            getVarchar(model.table_B0, 1, nOps, batch, s);
                        }
                    });
            }

            ops.add(
                new Op("setB0->A" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        setB0ToA(nOps, batch);
                    }
                });

            ops.add(
                new Op("navB0->A" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        navB0ToA(nOps, batch);
                    }
                });

            ops.add(
                new Op("navB0->A_alt" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        navB0ToAalt(nOps, batch);
                    }
                });

            // XXX exclude, NDB/J exceptions
            ops.add(
                new Op("navA->B0" + (forceSend ? "_forceSend" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        navAToB0(nOps, forceSend);
                    }
                });

            // XXX exclude, not implemented yet
            ops.add(
                new Op("navA->B0_alt" + (forceSend ? "_forceSend" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        navAToB0alt(nOps, forceSend);
                    }
                });

            ops.add(
                new Op("nullB0->A" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        nullB0ToA(nOps, batch);
                    }
                });

            ops.add(
                new Op("delB0ByPK" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        delByPK(model.table_B0, 1, nOps, batch);
                    }
                });

            ops.add(
                new Op("delAByPK" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        delByPK(model.table_A, 1, nOps, batch);
                    }
                });

            ops.add(
                new Op("insA_attr" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        ins(model.table_A, 1, nOps, setAttrs, batch);
                    }
                });

            ops.add(
                new Op("insB0_attr" + (batch ? "_batch" : "")) {
                    public void run(int nOps)
                        throws NdbApiException {
                        ins(model.table_B0, 1, nOps, setAttrs, batch);
                    }
                });

            ops.add(
                new Op("delAllB0") {
                    public void run(int nOps)
                        throws NdbApiException {
                        final int count = delByScan(model.table_B0);
                        assert count == nOps;
                    }
                });

            ops.add(
                new Op("delAllA") {
                    public void run(int nOps)
                        throws NdbApiException {
                        final int count = delByScan(model.table_A);
                        assert count == nOps;
                    }
                });
        }

        out.println(" [Op: " + ops.size() + "]");
    }

    protected void closeOperations() {
        out.print("closing operations ...");
        out.flush();
        ops.clear();
        out.println("      [ok]");
    }

    protected void beginTransaction() throws NdbApiException {
        // start a transaction
        // must be closed with NdbTransaction.close
        tx = ndb.startTransaction();
        assert tx != null;
    }

    protected void executeOperations() throws NdbApiException {
        // execute but don't commit the current transaction
        // XXX not documented: return value != 0 v throwing exception
        // YYY Monty: should always throw exception -> void method
        int stat = tx.execute(ExecType.NoCommit, AbortOption.AbortOnError);
        if (stat != 0)
            throw new RuntimeException("stat == " + stat);
    }

    protected void commitTransaction() throws NdbApiException {
        // commit the current transaction
        // XXX not documented: return value != 0 v throwing exception
        // YYY Monty: should always throw exception -> void method
        assert tx != null;
        int stat = tx.execute(ExecType.Commit, AbortOption.AbortOnError);
        if (stat != 0)
            throw new RuntimeException("stat == " + stat);
    }

    protected void rollbackTransaction() throws NdbApiException {
        // abort the current transaction
        // XXX not documented: return value != 0 v throwing exception
        // YYY Monty: should always throw exception -> void method
        int stat = tx.execute(ExecType.Rollback);
        if (stat != 0)
            throw new RuntimeException("stat == " + stat);
    }

    protected void closeTransaction() {
        // close the current transaction
        // to be called irrespectively of success or failure
        tx.close();
        tx = null;
    }

    // ----------------------------------------------------------------------

    protected void fetchCommonAttributes(NdbOperation op)
        throws NdbApiException {
        op.getValue(model.name_cint);
        op.getValue(model.name_clong);
        op.getValue(model.name_cfloat);
        op.getValue(model.name_cdouble);
    }

    protected int getCommonAttributes(NdbResultSet rs)
        throws NdbApiException {
        final int cint = rs.getInt(model.name_cint);
        final long clong = rs.getLong(model.name_clong);
        verify(clong == cint);
        final float cfloat = rs.getFloat(model.name_cfloat);
        verify(cfloat == cint);
        final double cdouble = rs.getDouble(model.name_cdouble);
        verify(cdouble == cint);
        return cint;
    }

    protected void ins(NdbTable table, int from, int to,
                       boolean setAttrs, boolean batch)
        throws NdbApiException {
        beginTransaction();
        for (int i = from; i <= to; i++) {
            // get an insert operation for the table
            final NdbOperation op = tx.getInsertOperation(table);
            assert op != null;

            // set key attribute
            op.equalInt(model.name_id, i);

            // set other attributes
            if (setAttrs) {
                op.setInt(model.name_cint, -i);
                op.setLong(model.name_clong, -i);
                op.setFloat(model.name_cfloat, -i);
                op.setDouble(model.name_cdouble, -i);
            }

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        commitTransaction();
        closeTransaction();
    }

    protected void delByPK(NdbTable table, int from, int to,
                           boolean batch)
        throws NdbApiException {
        beginTransaction();
        for (int i = from; i <= to; i++) {
            // get a delete operation for the table
            final NdbOperation op = tx.getDeleteOperation(table);
            assert op != null;

            // set key attribute
            op.equalInt(model.name_id, i);

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        commitTransaction();
        closeTransaction();
    }

    protected int delByScan(NdbTable table) throws NdbApiException {
        beginTransaction();

        // get a full table scan operation with exclusive locks
        final NdbScanOperation op
            = tx.getSelectScanOperation(table, LockMode.LM_Exclusive);
        assert op != null;

        // start the scan; don't commit yet
        executeOperations();

        // delete all rows in a given scan
        int count = 0;
        int stat;
        final boolean allowFetch = true; // request new batches when exhausted
        while ((stat = op.nextResult(allowFetch)) == 0) {
            // delete all tuples within a batch
            do {
                op.deleteCurrentTuple();
                count++;

                // XXX execute the operation now if in non-batching mode
                //if (!batch)
                //    executeOperations();
          } while ((stat = op.nextResult(!allowFetch)) == 0);

            if (stat == 1) {
                // no more batches
                break;
            }
            if (stat == 2) {
                // end of current batch, fetch next
                // XXX not documented: return value != 0 v throwing exception
                // YYY Monty: should always throw exception -> void method
                int s = tx.execute(ExecType.NoCommit, AbortOption.AbortOnError);
                if (s != 0)
                    throw new RuntimeException("s == " + s);
                continue;
            }
            throw new RuntimeException("stat == " + stat);
        }
        if (stat != 1)
            throw new RuntimeException("stat == " + stat);

        // close the scan
        op.close();

        commitTransaction();
        closeTransaction();
        return count;
    }

    protected void setByPK(NdbTable table, int from, int to,
                           boolean batch)
        throws NdbApiException {
        beginTransaction();
        for (int i = from; i <= to; i++) {
            // get an insert operation for the table
            final NdbOperation op = tx.getUpdateOperation(table);
            assert op != null;

            // set key attribute
            op.equalInt(model.name_id, i);

            // set other attributes
            op.setInt(model.name_cint, i);
            op.setLong(model.name_clong, i);
            op.setFloat(model.name_cfloat, i);
            op.setDouble(model.name_cdouble, i);

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        commitTransaction();
        closeTransaction();
    }

    // XXX need to use names instead of ids due to broken
    // NdbOperation.getValue(int), NdbResultSet.getXXX(int)
    protected void testBrokenGetValueByIndex()
        throws NdbApiException {
        assert tx == null;
        tx = ndb.startTransaction();
        assert tx != null;
        NdbOperation op = tx.getSelectOperation(model.table_A);
        assert op != null;
        final int int_val = 1;
        op.equalInt("id", int_val);
        op.getValue("id"); // XXX error with index instead of "id"
        final NdbResultSet rs = op.resultData();
        tx.execute(ExecType.Commit, AbortOption.AbortOnError, true);
        while (rs.next()) {
            int id = rs.getInt("id"); // XXX error with index instead of "id"
            assert id == int_val;
        }
        tx.close();
        tx = null;
    }

    protected void getByPK(NdbTable table, int from, int to,
                           boolean batch)
        throws NdbApiException {
        // operation results
        final int count = (to - from) + 1;
        final NdbResultSet[] rss = new NdbResultSet[count];

        beginTransaction();
        for (int i = 0, j = from; i < count; i++, j++) {
            // get a read operation for the table
            NdbOperation op = tx.getSelectOperation(table);
            assert op != null;

            // set key attribute
            op.equalInt(model.name_id, j);

            // define fetched attributes
            op.getValue(model.name_id);
            fetchCommonAttributes(op);

            // get attributes (not readable until after commit)
            rss[i] = op.resultData();

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        commitTransaction();

        // check fetched values
        for (int i = 0, j = from; i < count; i++, j++) {
            final NdbResultSet rs = rss[i];
            final boolean hasNext = rs.next();
            assert hasNext;

            // check key attribute
            final int id = rs.getInt(model.name_id);
            verify(id == j);

            // check other attributes
            final int id1 = getCommonAttributes(rs);
            verify(id1 == j);

            assert !rs.next();
        }
        closeTransaction();
    }

    protected void setVarbinary(NdbTable table, int from, int to,
                                boolean batch, byte[] bytes)
        throws NdbApiException {
        beginTransaction();
        for (int i = from; i <= to; i++) {
            // get an update operation for the table
            final NdbOperation op = tx.getUpdateOperation(table);
            assert op != null;

            // set key attribute
            op.equalInt(model.name_id, i);

            // set varbinary
            op.setBytes(model.name_B0_cvarbinary_def, bytes);

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        commitTransaction();
        closeTransaction();
    }

    protected void setVarchar(NdbTable table, int from, int to,
                              boolean batch, String string)
        throws NdbApiException {
        beginTransaction();
        for (int i = from; i <= to; i++) {
            // get an update operation for the table
            final NdbOperation op = tx.getUpdateOperation(table);
            assert op != null;

            // set key attribute
            op.equalInt(model.name_id, i);

            // set varchar
            op.setString(model.name_B0_cvarchar_def, string);

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        commitTransaction();
        closeTransaction();
    }

    protected void getVarbinary(NdbTable table, int from, int to,
                                boolean batch, byte[] bytes)
        throws NdbApiException {
        // operation results
        final int count = (to - from) + 1;
        final NdbResultSet[] rss = new NdbResultSet[count];

        beginTransaction();
        for (int i = 0, j = from; i < count; i++, j++) {
            // get a read operation for the table
            NdbOperation op = tx.getSelectOperation(table);
            assert op != null;

            // set key attribute
            op.equalInt(model.name_id, j);

            // define fetched attributes
            op.getValue(model.name_B0_cvarbinary_def);

            // get attributes (not readable until after commit)
            rss[i] = op.resultData();

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        //executeOperations();
        commitTransaction();

        // check fetched values
        for (int i = 0, j = from; i < count; i++, j++) {
            final NdbResultSet rs = rss[i];
            final boolean hasNext = rs.next();
            assert hasNext;

            // check varbinary
            final byte[] cvarbinary_def
                = rs.getBytes(model.name_B0_cvarbinary_def);
            verify(Arrays.equals(bytes, cvarbinary_def));

            assert !rs.next();
        }
        closeTransaction();
    }

    protected void getVarchar(NdbTable table, int from, int to,
                              boolean batch, String string)
        throws NdbApiException {
        // operation results
        final int count = (to - from) + 1;
        final NdbResultSet[] rss = new NdbResultSet[count];

        beginTransaction();
        for (int i = 0, j = from; i < count; i++, j++) {
            // get a read operation for the table
            NdbOperation op = tx.getSelectOperation(table);
            assert op != null;

            // set key attribute
            op.equalInt(model.name_id, j);

            // define fetched attributes
            op.getValue(model.name_B0_cvarchar_def);

            // get attributes (not readable until after commit)
            rss[i] = op.resultData();

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        //executeOperations();
        commitTransaction();

        // check fetched values
        for (int i = 0, j = from; i < count; i++, j++) {
            final NdbResultSet rs = rss[i];
            final boolean hasNext = rs.next();
            assert hasNext;

            // check varchar
            if (true) {
                final String cvarchar_def
                    = rs.getString(model.name_B0_cvarchar_def);
                verify(string.equals(cvarchar_def));
            } else {
                // verification imposes a string->bytes conversion penalty
                final byte[] cvarchar_def
                    = rs.getStringBytes(model.name_B0_cvarchar_def);
                verify(Arrays.equals(string.getBytes(), cvarchar_def));
            }

            assert !rs.next();
        }
        closeTransaction();
    }

    protected void setB0ToA(int nOps,
                            boolean batch)
        throws NdbApiException {
        beginTransaction();
        for (int i = 1; i <= nOps; i++) {
            // get an update operation for the table
            final NdbOperation op = tx.getUpdateOperation(model.table_B0);
            assert op != null;

            // set key attribute
            op.equalInt(model.name_id, i);

            // set a_id attribute
            int a_id = ((i - 1) % nOps) + 1;
            op.setInt(model.name_B0_a_id, a_id);

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        commitTransaction();
        closeTransaction();
    }

    protected void nullB0ToA(int nOps,
                             boolean batch)
        throws NdbApiException {
        beginTransaction();
        for (int i = 1; i <= nOps; i++) {
            // get an update operation for the table
            final NdbOperation op = tx.getUpdateOperation(model.table_B0);
            assert op != null;

            // set key attribute
            op.equalInt(model.name_id, i);

            // set a_id attribute
            int a_id = ((i - 1) % nOps) + 1;
            op.setNull(model.name_B0_a_id);

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        commitTransaction();
        closeTransaction();
    }

    protected void navB0ToA(int nOps,
                            boolean batch)
        throws NdbApiException {
        beginTransaction();

        // fetch the foreign keys from B0 and read attributes from A
        final NdbResultSet[] abs = new NdbResultSet[nOps];
        for (int i = 1, j = 0; i <= nOps; i++, j++) {
            // fetch the foreign key value from B0
            NdbResultSet rs;
            {
                // get a read operation for the table
                NdbOperation op = tx.getSelectOperation(model.table_B0);
                assert op != null;

                // set key attribute
                op.equalInt(model.name_id, i);

                // define fetched attributes
                op.getValue(model.name_B0_a_id);

                // get attributes (not readable until after commit)
                rs = op.resultData();
            }
            executeOperations(); // start the scan; don't commit yet

            // fetch the attributes from A
            {
                // get a read operation for the table
                NdbOperation op = tx.getSelectOperation(model.table_A);
                assert op != null;

                // set key attribute
                final int a_id = rs.getInt(model.name_B0_a_id);
                assert a_id == ((i - 1) % nOps) + 1;
                op.equalInt(model.name_id, a_id);

                // define fetched attributes
                op.getValue(model.name_id);
                fetchCommonAttributes(op);

                // get attributes (not readable until after commit)
                abs[j] = op.resultData();
            }

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        commitTransaction();

        // check fetched values
        for (int i = 1, j = 0; i <= nOps; i++, j++) {
            final NdbResultSet ab = abs[j];
            final boolean hasNext = ab.next();
            assert hasNext;

            // check key attribute
            final int id = ab.getInt(model.name_id);
            //out.println("id = " + id + ", i = " + i);
            verify(id == ((i - 1) % nOps) + 1);

            // check other attributes
            final int k = getCommonAttributes(ab);
            verify(k == id);

            assert !ab.next();
        }
        closeTransaction();
    }

    protected void navB0ToAalt(int nOps,
                               boolean batch)
        throws NdbApiException {
        beginTransaction();

        // fetch the foreign key value from B0
        final NdbResultSet[] a_ids = new NdbResultSet[nOps];
        for (int i = 1, j = 0; i <= nOps; i++, j++) {
                // get a read operation for the table
                NdbOperation op = tx.getSelectOperation(model.table_B0);
                assert op != null;

                // set key attribute
                op.equalInt(model.name_id, i);

                // define fetched attributes
                op.getValue(model.name_B0_a_id);

                // get attributes (not readable until after commit)
                a_ids[j] = op.resultData();
        }
        executeOperations(); // start the scan; don't commit yet

        // fetch the attributes from A
        final NdbResultSet[] abs = new NdbResultSet[nOps];
        for (int i = 1, j = 0; i <= nOps; i++, j++) {
            // get a read operation for the table
            NdbOperation op = tx.getSelectOperation(model.table_A);
            assert op != null;

            // set key attribute
            final int a_id = a_ids[j].getInt(model.name_B0_a_id);
            assert a_id == ((i - 1) % nOps) + 1;
            op.equalInt(model.name_id, a_id);

            // define fetched attributes
            op.getValue(model.name_id);
            fetchCommonAttributes(op);

            // get attributes (not readable until after commit)
            abs[j] = op.resultData();

            // execute the operation now if in non-batching mode
            if (!batch)
                executeOperations();
        }
        commitTransaction();

        // check fetched values
        for (int i = 1, j = 0; i <= nOps; i++, j++) {
            final NdbResultSet ab = abs[j];
            final boolean hasNext = ab.next();
            assert hasNext;

            // check key attribute
            final int id = ab.getInt(model.name_id);
            //out.println("id = " + id + ", i = " + i);
            verify(id == ((i - 1) % nOps) + 1);

            // check other attributes
            final int k = getCommonAttributes(ab);
            verify(k == id);

            assert !ab.next();
        }
        closeTransaction();
    }

    protected void navAToB0(int nOps,
                            boolean forceSend)
        throws NdbApiException {
// throws exceptions, see below:
/*
        beginTransaction();

        // fetch attributes from B0 by foreign key scan
        final NdbResultSet[] abs = new NdbResultSet[nOps];
        int j = 0;
        for (int i = 1; i <= nOps; i++) {
            // get an index scan operation for the table
            // XXX ? no locks (LM_CommittedRead) or shared locks (LM_Read)
            final NdbIndexScanOperation op
                = tx.getSelectIndexScanOperation(model.idx_B0_a_id,
                                                 model.table_B0,
                                                 LockMode.LM_CommittedRead);
            assert op != null;

            // define the scan's bounds (faster than using a scan filter)
            // XXX this hardwired column name isn't right
            //op.setBoundInt("a_id", BoundType.BoundEQ, i);
            // compare with Operations.cpp:
            //    if (op->setBound(idx_B0_a_id->getColumn(0)->getAttrId()...
            //
            // which translates into
            //out.println("idx_B0_a_id.getNoOfColumns() = "
            //            + model.idx_B0_a_id.getNoOfColumns());
            //out.println("idx_B0_a_id.getColumn(0).getColumnNo() = "
            //            + model.idx_B0_a_id.getColumn(0).getColumnNo());
            //op.setBoundInt(model.idx_B0_a_id.getColumn(0).getColumnNo(),
            //               BoundType.BoundEQ, i);
            // except that we get the usual error with NDBJ:
            //[java] idx_B0_a_id.getColumn(0).getColumnNo() = 0
            //    [java] caught com.mysql.cluster.ndbj.NdbApiException:
            //           Invalid attribute name or number
            //
            // so we go by column name
            //out.println("idx_B0_a_id.getColumn(0).getName() = "
            //            + model.idx_B0_a_id.getColumn(0).getName());
            //op.setBoundInt(model.idx_B0_a_id.getColumn(0).getName(),
            //               BoundType.BoundEQ, i);
            // which is actually "a_id", so, for now, we call
            op.setBoundInt("a_id", BoundType.BoundEQ, i);

            // define fetched attributes
            op.getValue(model.name_id);
            fetchCommonAttributes(op);

            // start the scan; don't commit yet
            executeOperations();

            int stat;
            final boolean allowFetch = true; // request new batches when exhausted
            while ((stat = op.nextResult(allowFetch, forceSend)) == 0) {
                // get attributes (not readable until after commit)
                abs[j++] = op.resultData();
            }
            if (stat != 1)
                throw new RuntimeException("stat == " + stat);
        }
        commitTransaction();
        assert (j++ == nOps);

        // check fetched values
        j = 0;
        for (int i = 1; i <= nOps; i++) {
            final NdbResultSet ab = abs[j++];
            //out.println("j = " + j + ", ab = " + ab);
            //final boolean hasNext = ab.next();
            // throws
            //[java] j = 1, ab = com.mysql.cluster.ndbj.NdbResultSetImpl@6f144c
            //    [java] caught com.mysql.cluster.ndbj.NdbApiException: Unknown error code
            //    [java] com.mysql.cluster.ndbj.NdbApiException: Unknown error code
            //    [java] at com.mysql.cluster.ndbj.NdbjJNI.NdbScanOperationImpl_nextResult__SWIG_(Native Method)
            //    [java] at com.mysql.cluster.ndbj.NdbScanOperationImpl.nextResult(NdbScanOperationImpl.java:93)
            //    [java] at com.mysql.cluster.ndbj.NdbResultSetImpl.next(NdbResultSetImpl.java:362)
            //    [java] at com.mysql.cluster.crund.NdbjLoad.navAToB0(NdbjLoad.java:1205)
            //
            // YYY Frazer: check tx object for error (could be node failure)
            // Martin: doesn't help much; after ab.next():
            //out.println("tx.getNdbError() = " + tx.getNdbError().getCode());
            // returns -1 and
            //out.println("tx.getNdbError() = " + tx.getNdbError().getMessage());
            // says "Unknown error code"
            //
            // apparently,
            //final boolean hasNext = ab.next();
            // is the same as
            //final boolean hasNext = ab.next(true);
            // this returns false, but throws no exception:
            //final boolean hasNext = ab.next(false);
            out.println("tx.getNdbError() = " + tx.getNdbError().getCode());
            final boolean hasNext = ab.next();
            assert hasNext;

            // check key attribute
            final int id = ab.getInt(model.name_id);
            verify(id == i);

            // check other attributes
            final int id1 = getCommonAttributes(ab);
            verify(id1 == i);

            assert !ab.next();
        }
        closeTransaction();
*/
    }

    protected void navAToB0alt(int nOps,
                               boolean forceSend)
        throws NdbApiException {
// XXX not implemented yet, fix exception in navAToB0() first
/*
        assert false;
*/
    }

    // ----------------------------------------------------------------------
    // NDBJ datastore operations
    // ----------------------------------------------------------------------

    protected void initConnection() throws NdbApiException {
        out.println();

        // optionally, connect and wait for reaching the data nodes (ndbds)
        out.print("waiting for ndbd ...");
        out.flush();
        final int initial_wait = 10; // secs to wait until first node detected
        final int final_wait = 0;    // secs to wait after first node detected

        // XXX return: 0 all nodes live, > 0 at least one node live, < 0 error
        try {
            mgmd.waitUntilReady(initial_wait, final_wait);
        } catch (NdbApiException e) {
            out.println();
            out.println("!!! data nodes were not ready within "
                        + (initial_wait + final_wait) + "s.");
            throw e;
        }
        out.println("            [ok]");

        // connect to database
        out.print("connecting to ndbd ...");
        out.flush();
        try {
            // XXX where to set schema?
            // YYY Frazer: schema not too useful in NDB at the moment
            // XXX unclear if maxThreads ^= maxNumberOfTransactions
            //     since ndb.init(maxNumberOfTransactions) is deprecated
            //final int maxThreads = 4;
            //ndb = mgmd.createNdb(catalog, maxThreads);
            // YYY Frazer: yes, maxThreads == maxNumber(concurrent)OfTransactions
            ndb = mgmd.createNdb(catalog);
        } catch (NdbApiException e) {
            out.println();
            out.println("!!! failed to connect: " + e);
            throw e;
        }
        out.println("          [ok]");

        // initialize the schema shortcuts
        model = new Model(ndb);
    }

    protected void closeConnection() {
        out.println();
        out.print("closing ndbd connection ...");
        out.flush();
        model = null;
        ndb.close();
        ndb = null;
        out.println("     [ok]");
    }

    protected void clearData() throws NdbApiException {
        out.print("deleting all rows ...");
        out.flush();
        final int delB0 = delByScan(model.table_B0);
        out.print("           [B0: " + delB0);
        out.flush();
        final int delA = delByScan(model.table_A);
        out.print(", A: " + delA);
        out.flush();
        out.println("]");
    }

    // ----------------------------------------------------------------------

    static public void main(String[] args) {
        System.out.println("NdbjLoad.main()");
        parseArguments(args);
        new NdbjLoad().run();
        System.out.println();
        System.out.println("NdbjLoad.main(): done.");
    }
}
