Source: SeLite/db-objects/src/chrome/content/DbStorage.js

/*  Copyright 2011, 2012, 2013, 2014 Peter Kehl
    This file is part of SeLite Db Objects.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

"use strict";

Components.utils.import( 'chrome://selite-misc/content/SeLiteMisc.js' );
Components.utils.import("chrome://selite-sqlite-connection-manager/content/SqliteConnectionManager.js");
Components.utils.import("chrome://selite-settings/content/SeLiteSettings.js" );
Components.utils.import( 'chrome://selite-db-objects/content/Db.js' );
var console= Components.utils.import("resource://gre/modules/Console.jsm", {}).console;

/** This provides low-level data functions. It's a constructor of an object,
 *  but the code is procedural. The reason to have it as an object is not to
 *  have name clashes with functions in other files. See SeLite DB Objects for OOP layer on top of this.
 **/
SeLiteData.Storage= function Storage() {
    /** @type {SQLiteConnectionParameters} */
    this.parameters= new SQLiteConnectionParameters();
    this.parameters.errorHandler= console.error;
    this.con= null; // This will be the actual connection - result of Services.storage.openDatabase(file)
};

/** @return SQLite connection or null */
SeLiteData.Storage.prototype.connection= function connection() { return this.con; };

/** @return {string} Table prefix, or an empty string. It never returns undefined.
 *  SeLiteData.Storage doesn't actively use tablePrefix.
 *  When SeLiteData.Storage receives a call to update a record, e.g. SeLiteData.Storage.insertRecord(..),
 *  it doesn't add this tablePrefix to any of the table names - the client must do that. That's because
 *  SeLiteData.Storage doesn't modify queries passed from the client either.
 *  Overriden in private subclass StorageFromSettings.
 * */
SeLiteData.Storage.prototype.tablePrefix= function tablePrefix() { return ''; };

/** Open a new SQLite connection, as specified in parameters. Set it as the connection for this object.
 * Note: if you use this.connection().clone(), it won't inherit PRAGMA locking_mode
 * @return the new connection
 */
SeLiteData.Storage.prototype.open= function open() {
    !this.con || SeLiteMisc.fail( "Already connected to " +this.parameters.fileName );
    this.con= this.parameters.connect();
};

/** Close the connection.
 *  @param {boolean} synchronous Whether to close it ; optional.
 *  @param {function} [callback] Function to call after closed (regardless whether synchronous or asynchronous close)
 *  @TODO check: If you have used any asynchronous (?!) statements, pass true. See same comment in SQLiteConnectionInfo.close()
 * */
SeLiteData.Storage.prototype.close= function close( synchronous, callback ) {
    this.parameters.close( synchronous, callback );
    this.con= null;
};

/** This gets the list of result column names (e.g. names after the last space, if present; otherwise after the last dot, if present)
 *  out of column expressions/parts.
 *  @param array of strings
 *  @return array of strings
 *  For internal use only.
 **/
SeLiteData.Storage.prototype.fieldNames= function fieldNames( columnParts ) {
    var result= [];
    for( var i=0; i<columnParts.length; i++ ) {
        result[i]= this.fieldName( columnParts[i] );
    }
    return result;
};

SeLiteData.Storage.prototype.fieldName= function fieldName( columnPart ) {
    var result= columnPart.trim();
    var lastSpaceIndex= result.lastIndexOf(' ');
    if( lastSpaceIndex>0 ) {
        result= result.substr( lastSpaceIndex+1 );
    }
    else {
        var lastDotIndex= result.lastIndexOf('.');
        if( lastDotIndex>0 ) {
            result= result.substr( lastDotIndex+1 );
        }
    }
    // Remove any leading and trailing back apostrophe:
    if( result.length>1 && result[0]=='`' && result[result.length]=='`' ) {
        result= result.substr( 1, result.length-2 );
    }
    return result;
};

/** This collects fields from within SELECT... FROM part of the given query.
 *  For internal use only.
 **/
SeLiteData.Storage.prototype.fieldParts= function fieldParts( query ) {
    // Make lowercase. Make one or more whitespaces be a single space - so that we can locate 'select' and 'from' etc. easily.
    query= query.toLowerCase().replace( /\s+/g, ' ');
    var selectStart= query.indexOf( 'select ');
    if( selectStart<0 ) {
        throw "Query \"" +query+ "\" doesn't contain SELECT.";
    }
    var fromStart= query.indexOf( ' from ', selectStart );
    if( fromStart<0 ) {
        throw "Query \"" +query+ "\" doesn't contain FROM.";
    }
    var selectFrom= query.substring( selectStart+7, fromStart );
    if( selectFrom.indexOf('*')>=0 ) {
        throw "Query \"" +query+ "\" selects columns using *.";
    }
    var fieldParts= selectFrom.split( / ?, ?/ );
    var fields= [];
    for( var i=0; i<fieldParts.length; i++ ) {
        var part= fieldParts[i];
        if( part==='' ) {
            throw "Query \"" +query+ "\" has an empty " +(i+1)+ "-th part of SELECT list.";
        }
        var lastSpace= part.lastIndexOf( ' ' );
        if( lastSpace>=0 ) {
            if( lastSpace==part.length-1 ) {
                throw "Query \"" +query+ "\" has strange " +(i+1)+ "-th part of SELECT list.";
            }
            part= part.substr( lastSpace+1);
        }
        fields.push( part );
    }
    if( fields.length==0 ) {
        throw "Query \"" +query+ "\" has an empty SELECT list.";
    }
    return fields;
};

/** @return string SQL condition containing the given first and second condition(s), whichever of them is present and not empty;
 *  an empty string if there's no condition at all.
 *   This doesn't put oval parenthesis around the condition parts.
 *   It's not the same as [condition1, condition2... ].join( ' AND ' ), because that would put 'AND' between all
 *   parameters, even if some are empty.
 **/
SeLiteData.Storage.prototype.sqlAnd= function sqlAnd( first, second, etc ) {
    var argumentsPresent= [];
    for( var i=0; i<arguments.length; i++ ) {
        if( arguments[i] ) {
            argumentsPresent.push( arguments[i] );
        }
    }
    return argumentsPresent.join( ' AND ' );
};

/** @param string query SQL query. It can contain placeholders in format :placeholderName
 *  (see https://developer.mozilla.org/en/docs/Storage > Binding Parameters.
 *  @param object bindings Optional; Object (serving as associative array) of parameters to bind,
 *  i.e. bindings to replace placeholders in the query. When using the named parameter(s) in the query, prefix them it with a colon - e.g. access parameter xyz as :xyz. See https://developer.mozilla.org/en/Storage#Binding_One_Set_of_Parameters.
 *  @param array fields Optional; array of strings SQL fields (columns) to collect; must match case-sensitively columns
 *  coming from the query. If not present,
 *  then this function will try to collect the column names from the given query, providing
 *  - all required column names are listed, there's no *
 *  - none of the column names is string 'FROM' (case insensitive)
 *  - there are no sub-selects between SELECT...FROM
 *  @return array of objects, one per DB row, each having DB column names as fields; empty array if no matching rows
 *  @throws error on failure
 **/
SeLiteData.Storage.prototype.select= function select( query, bindings={}, fields ) {
    if( !fields ) {
        fields= this.fieldNames( this.fieldParts( query ) );
    }
    this.connection() || SeLiteMisc.fail( 'SeLiteData.Storage.connection() is not set. SQLite file name: ' +this.parameters.fileName );
    //console.log( 'SeLite: ' +query );
    var stmt= this.connection().createStatement( query );
    for( var field in bindings ) {
        try {
            stmt.params[field]= bindings[field];
        }
        catch(e) {
            throw 'select(): Cannot set field "' +field+ '" to value "' +bindings[field]+ '" - SQLite/schema limitation.';
        }
    }
    var result= [];
    try {
        // MDN Docs on synchronous API are not detailed enough. If iterating over SELECT with stmt.executeStep()
        // - you can't use: for( var field in stmt.row ) - so you need to know the list of columns
        // - fields are accessbile via stmt.row.<column-name> only before you reset the statement!
        while( stmt.executeStep() ) {
            var resultRow= {};
            for( var i=0; i<fields.length; i++ ) {
                resultRow[ fields[i] ]= stmt.row[ fields[i] ];
            }
            result.push( resultRow );
        }
    }
    finally {
        stmt.finalize();
    }
    return result;
};

/** It selects 1 row from the DB. If there are no such rows, or more than one, then it throws an error.
 *  @param {string} query Full SQL query
 *  @param {object} bindings Object serving as an associative array, with bindings for named parameters; optional. see the same parameter of select(). When using the named parameter(s) in the query, prefix them it with a colon - e.g. access parameter xyz as :xyz. See https://developer.mozilla.org/en/Storage#Binding_One_Set_of_Parameters.
 *  @param array fields Optional (unless you use SELECT * etc.); optional - see the same parameter of select().
 *  @return Object (serving as an associative array) for the row.
 *  @throws error on failure
 **/
SeLiteData.Storage.prototype.selectOne= function selectOne( query, bindings, fields ) {
    var rows= this.select( query, bindings, fields );
    if( rows.length!=1 ) {
        throw "Query \"" +query+"\" was supposed to return one result, but it returned " +rows.length+ " of them.";
    }
    return rows[0];
};

/** @param string query SQL query
 *  @param object bindings Optional; see select()
 *  @return void
 *  @throws error on failure
 **/
SeLiteData.Storage.prototype.execute= function execute( query, bindings ) {
    if( !bindings ) {
        this.connection().executeSimpleSQL( query );
    }
    else {
        var stmt= this.connection().createStatement( query );
        for( var field in bindings ) {
            stmt.params[field]= bindings[field];
        }
        try {
            stmt.execute();
        }
        finally {
            stmt.finalize();
        }
    }
};

/** Get one record from the DB.
 * @param object params Object just like the same-called parameter for getRecords()
 * @return object of the DB record
 * @throws error on failure, or if no such record, or if more than 1 matching record
 **/
SeLiteData.Storage.prototype.getRecord= function getRecord( params ) {
    var records= this.getRecords( params );
    if( records.length!=1 ) {
        throw "Expected to find one matching record in DB, but found " +records.length+ " of them.";
    }
    return records[0];
};

/** Get (matching) records from the DB.
 * @param {Object} params Object in form {
 *   table: string table name,
 *   columns: mixed - one of
 *     - array of string table names; or
 *     - string containing columns names separated by a comma
 *        or by a comma and space(s). The column names can be simple e.g. 'id'; or SQL column labels e.g. 'created created_time'
 *        or 'created AS created_time', or 'sq-expression-here column-alias-here'
 *        - but if it's not a simple name then you have to use the alias part; or
 *     - object containg DB column names as keys (field names), and their aliases as values or true/1 if no alias to use (then it uses the column name).
 *   joins: optional, array of objects [
 *      {
 *        table: string table name (it may be '<tablename> <tablealias>', then you don't need to use alias field, but alias field is preferred),
 *        alias: string table alias (optional),
 *        type: string 'INNER LEFT' etc.; optional,
 *        on: string join condition without 'ON' itself, optional
 *      }
 *   ],
 *   matching: optional, object {
 *     // String values will be quoted, unless they start with ':' and contain a parameter name listed in parameterNames.
 *     // Javascript null value will generate IS NULL, other values generate = comparison.
 *     fieldXY: value, fieldPQ: value...
 *   },
 *   condition: optional, string SQL condition, properly quoted. Both 'matching' and 'condition'
 *     may be used at the same time, then they are AND-ed,
 *   parameters: object serving as an associative array, listing parameters in form {string parameter-name: mixed parameter-value, ...}. Optional.
 *   parameterNames: array of strings which are acceptable parameter names. Optional, but should be set if you want 'parameters' to be applied.
 *   sort: optional, string column name to sort by
 *   sortDirection: optional, string direction to sort by; 'ASC' by default (if sorting)
 *   debugQuery: optional, use true if you'd like it to show the query in a popup
 *   debugResult: optional, use true if you'd like it to show the result in a popup
 * }
 **/
SeLiteData.Storage.prototype.getRecords= function getRecords( params ) {
    var parameterNames= params.parameterNames || {};
    if( typeof params.columns=='string' ) {
        var columnsString= params.columns;
        var columnsList= params.columns.split( / ?, ?/ );
    }
    else if( Array.isArray(params.columns) ) {
        var columnsString= params.columns.join(', ');
        var columnsList= params.columns;
    }
    else if( typeof params.columns ==='object' ) {
        var columnsString= '';
        var columnsList= [];
        for( var column in params.columns ) {
            if( columnsString ) {
                columnsString+= ', ';
            }
            if( typeof params.columns[column] ==='string' ) {
                columnsString+= column+ ' ' +params.columns[column];
                columnsList.push( params.columns[column] );
            }
            else
            if( params.columns[column]===true || params.columns[column]===1 ) {
                columnsString+= column;
                columnsList.push( this.fieldName(column) );
            }
            else {
                throw new Error( "getRecords(): column '" +column+ "' has an unsupported non-string alias " +params.columns[column] );
            }
        }
    }
    else throw new Error( "params.columns not recognised" );
    columnsList= this.fieldNames( columnsList );
    var query= "SELECT " +columnsString+ " FROM " +params.table;

    if( params.joins!==undefined ) {
        for( var i=0; i<params.joins.length; i++ ) {
            var join= params.joins[i];
            if( join.type!==undefined ) {
                query+= ' ' +join.type;
            }
            query+= ' JOIN ' +join.table;
            if( join.alias!==undefined ) {
                query+= ' ' +join.alias;
            }
            if( join.on!==undefined ) {
                query+= " ON " +join.on;
            }
        }
    }

    var conditionParts= [];
    if( params['matching']!==undefined && !SeLiteMisc.isEmptyObject(params['matching']) ) {
        for( var field in params.matching ) {
            var matchedValue= params.matching[field];
            matchedValue= typeof matchedValue==='string' && matchedValue.length>1
                && matchedValue[0]===':' && parameterNames.indexOf(matchedValue.substr(1))>=0
                ? matchedValue // Don't quote SQLite parameters :xyz - SQLite will escape & quote automatically
                : this.quote( matchedValue );
            if( matchedValue!==null ) {
                conditionParts.push( field+ '=' +matchedValue );
            }
            else {
                conditionParts.push( field+ ' IS NULL' );
            }
        }
    }
    if( params['condition']!==undefined && params['condition']!=='' && params['condition']!=null ) {
        conditionParts.push( '('+params.condition+')' );
    }
    if( conditionParts.length ) {
        query+= " WHERE " +conditionParts.join(' AND ');
    }
    if( typeof params['sort']!=='undefined' && params.sort ) {
        var sortDirection= (params['sortDirection']===undefined || !params.sortDirection)
            ? 'ASC'
            : params.sortDirection;
        query+= " ORDER BY " +params.sort+ ' ' +sortDirection;
    }
    if( params.debugQuery!==undefined && params.debugQuery ) {
        console.log( query );
    }

    var result= this.select( query, params.parameters, columnsList );
    if( params.debugResult!==undefined && params.debugResult ) {
        console.log( SeLiteMisc.rowsToString(result) );
    }
    return result;
};

/** Update (matching) records in the DB.
 * @param {object} params Object in form {
 *   table: string table name,
 *   entries: object (serving as an associative array) with the columns to update in the given table. In form
 *       { field: value,
 *         field: value...
 *       }. The values will be quoted.
 *   fieldsToProtect: optional, array of strings, which are names of fields whose
 *       values won't be quoted. Use for SQL expressions or for values that were already securely quoted
 *   matching: optional, object (all values will be quoted) {
 *     fieldXY: value, fieldPQ: value...
 *   },
 *   condition: optional, string SQL condition, properly quoted. Both 'matching' and 'condition'
 *     may be used at the same time, then they are AND-ed.
 *   debugQuery: optional, use true if you'd like it to show the query in a popup
 * }
 * @return void
 * @throws an error on failure
 **/
SeLiteData.Storage.prototype.updateRecords= function updateRecords( params ) {
    var fieldsToProtect= params.fieldsToProtect!==undefined
        ? params.fieldsToProtect
        : [];
        //@TODO use bindings instead of quoting?:
    var entries= this.quoteValues( params.entries, fieldsToProtect );

    var setPairs= [];
    for( var field in entries ) {
        setPairs.push( field+ '=' +entries[field] );
    }
    if( !setPairs.length ) {
        console.error( 'updateRecords() requires params.entries not to be empty.' );
    }
    var query= "UPDATE " +params.table+ " SET " +setPairs.join(', ');

    var conditionParts= [];
    if( typeof params['matching'] !=='undefined' ) {
        for( var field in params.matching ) {
            conditionParts.push( field+ '=' +this.quote( params.matching[field] ) );
        }
    }
    if( params['condition']!==undefined && params['condition']!=='' && params['condition']!=null ) {
        conditionParts.push( '('+params.condition+')' );
    }
    if( conditionParts.length ) {
        query+= " WHERE " +conditionParts.join(' AND ');
    }

    if( params.debugQuery!==undefined && params.debugQuery ) {
        console.log( query );
    }
    var stmt= this.connection().createStatement( query );
    try {
        stmt.execute();
    }
    finally {
        stmt.finalize();
    }
};

/** Update a in the DB, matching it by 'id' field (i.e. params.entries.id).
 * @param {object} params Object in form {
 *   table: string table name,
 *   primary: string primary key name, or an array of string column names (for a multi-column primary key),
 *   entries: object (serving as an associative array) with the columns to update in the given table. In form
 *       { field: value,
 *         field: value...
 *       }. The values will be quoted.
 *   entries must contain '<primary>' field(s), which will be matched in the DB (but, indeed, the primary field(s) won't be updated).
 *   fieldsToProtect: optional, array of strings, which are names of fields whose
 *       values won't be quoted. Use for SQL expressions or for values that were already securely quoted
 *   debugQuery: optional, use true if you'd like it to show the query in a popup
 *   }
 * @return void
 * @throws an error on failure
 */
SeLiteData.Storage.prototype.updateRecordByPrimary= function updateRecordByPrimary( params ) {
    var copiedParams= SeLiteMisc.objectClone( params, ['table', 'entries', 'fieldsToProtect', 'debugQuery'], ['table', 'entries'] );
    copiedParams.entries= SeLiteMisc.objectClone( copiedParams.entries );
    var primaryKey= params.primary;
    var primaryKeyColumns= typeof primaryKey==='string'
        ? [primaryKey]
        : primaryKey;
    copiedParams.matching= {};
    for( var column of primaryKeyColumns ) {
        copiedParams.entries[column]!==undefined || SeLiteMisc.fail( "updateRecordByPrimary(): params.entries." +column+ " is not set." );
        copiedParams.matching[column]= copiedParams.entries[column];
        delete copiedParams.entries[column];
    }
    this.updateRecords( copiedParams );
};

/** Delete a record in the DB, matching it by the given field and its value.
 *  The value will be quoted and it must not be null.
 * @param {string} tableName
 * @param {string|array} primaryKey
 * @param {object} record
 * @return void
 * @throws an error on failure
 */
SeLiteData.Storage.prototype.removeRecordByPrimary= function removeRecordByPrimary( tableName, primaryKey, record ) {
    if( typeof primaryKey==='string' ) {
        primaryKey= [primaryKey];
    }
    var conditionParts= [];
    for( var primaryKeyColumn of primaryKey ) {
        conditionParts.push( primaryKeyColumn+ '=' +this.quoteValues( record[primaryKeyColumn] ) );
    }
    var query= "DELETE FROM " +tableName+ " WHERE " +conditionParts.join( 'AND' );
    var stmt= this.connection().createStatement( query );
    try {
        stmt.execute();
    }
    finally {
        stmt.finalize();
    }
};

/**Insert the  record into the DB.
 * @param {object} params Object in form {
 *   table: string table name,
 *   entries: object (serving as an associative array) to store in the given table. In form
 *       { field: value,
 *         field: value...
 *       }. The values will be quoted.
 *   fieldsToProtect: optional, array of strings, which are names of fields whose
 *       values won't be quoted. Use for SQL expressions or for values that were already securely quoted
 *   debugQuery: optional; use true if you'd like to show the query in a popup
 * }
 * @return void
 * @throws an error on failure
 */
SeLiteData.Storage.prototype.insertRecord= function insertRecord( params ) {
    var fieldsToProtect= params.fieldsToProtect!==undefined
        ? params.fieldsToProtect
        : [];
    var entries= this.quoteValues( params.entries, fieldsToProtect );
    var columns= [];
    var values= [];
    for( var field in entries ) {
        columns.push( field );
        values.push( entries[field] );
    }
    //@TODO re-do/merge with SeLiteData.Table.prototype.insert
    var query= 'INSERT INTO ' +params.table+ '(' +columns.join(', ')+ ') VALUES ('+
        values.join(', ')+ ')';
    if( params.debugQuery!==undefined && params.debugQuery ) {
        console.log( query );
    }
    var stmt= this.connection().createStatement( query );
    try {
        stmt.execute();
    }
    finally {
        stmt.finalize();
    }
};

/** This returns the last successfully inserted record.
 *  It only works when called right after the relevant INSERT statement, i.e. there must
 *  be no other INSERT statements before calling this, even if those INSERT statements would
 *  insert into a different table.
 *  It requires the DB table not to have any column called "rowid", "oid" or "_rowid_".
 *  See http://sqlite.org/lang_corefunc.html and http://sqlite.org/lang_createtable.html#rowid
 *  @param {string} tableName Table name (including any prefix); it must be the table that was used with the last INSERT/UPDATE.
 *  @param {(array)} columns Names of the columns to retrieve; optional - ['id'] by default
 *  @return {object} Record object with given column(s).
*/
SeLiteData.Storage.prototype.lastInsertedRow= function lastInsertedRow( tableName, columns=['id'] ) {
    var query= "SELECT " +columns.join(',')+ " FROM " +tableName+ " WHERE rowid=last_insert_rowid()";
    var record= this.selectOne( query, {}, columns );
    return record;
};

/** This adds enclosing apostrophes and doubles any apostrophes in the given value, if it's a string.
 *  <br>It does add enclosing apostrophes around Javascript strings "null" or "NULL" etc! So use Javascript null instead.
 *  Use SqlExpression if you don't want quotes to be added.
 *  <br>For Javascript null, it returns unqouted string 'NULL'. That way we can pass values part of the result of quoteValues()
 *  (which returns an array of quoted items) through Array.join(). Otherwise, if we kept Javascript null values as they were,
 *  we couldn't use Array.join(), because it uses empty strings for Javascript nulls! See functions that call quoteValues().
 */
SeLiteData.Storage.prototype.quote= function quote( value ) {
    if( value===null ) {
        return 'NULL';
    }
    if( value instanceof SeLiteData.SqlExpression ) {
        return ''+value;
    }
    return "'" +(''+value).replace( "'", "''" )+ "'";
};

/** Use instances of SeLiteData.SqlExpression so that they don't get quoted/escaped by quote() and quoteValues().
 **/
SeLiteData.SqlExpression= function SqlExpression( string ) {
    this.string= string;
};
SeLiteData.SqlExpression.prototype.toString= function toString() {
    return this.string;
};

/** This adds enclosing apostrophes and doubles any apostrophes in values in entries for SQLite. See quote() for details on escaping.
 *  @param {(Array|object)} entries it can be an array (created using new Array() or [...]), or an object {...} serving as an associative array
 *  @param {Array} fieldsToProtect Optional array of strings, which are field (column) names for fields that shouldn't be quoted. Use it
 *  if passing SQL expressions for their values. fieldToProtect has only effect if entries is an object.
 *  @return {(Array|object)} New array or object, based on the original, with the treated values. Original array/object is not modified.
 **/
SeLiteData.Storage.prototype.quoteValues= function quoteValues( entries, fieldsToProtect=[] ) {
    if( typeof entries !='object' ) {
        throw "quoteValues(): parameter should be an object or array, but it was " +values;
    }
    if( Array.isArray(entries) ) {
        var result= [];
        for( var i=0; i<entries.length; i++ ) {
            result[i]= this.quote( entries[i] );
        }
    }
    else {
        var result= {};
        for( var field in entries ) {
            if( fieldsToProtect.indexOf(field)>=0 ) {
                result[field]= entries[field];
            }
            else {
                result[field]= this.quote( entries[field] );
            }
        }
    }
    return result;
};

/** @constructs Subclass of SeLiteData.Storage, that is based on SeLiteSettings.Field pointing to an SQLite source, and an optional Field indicating table prefix.
 * @private
 *  @param {SeLiteSettings.Field.SQLite} dbField
 *  @param {SeLiteSettings.Field.String} [tablePrefixField]
 * */
function StorageFromSettings( dbField, tablePrefixField ) {
    SeLiteData.Storage.call( this );

    !(dbField.name in StorageFromSettings.instances) || SeLiteMisc.fail('There already is an instance of StorageFromSettings for ' +dbField.name );
    this.dbField= dbField;
    this.tablePrefixField= tablePrefixField;
    this.tablePrefixValue= undefined; // This will be set to value of Field instance stored in this.tablePrefixField for current test suite (once known). Tests must use .tablePrefix() instead of accessing .tablePrefixValue directly.
    
    StorageFromSettings.instances[ dbField.name ]= this;
    // I don't call this.open() here, because the test suite folder may not be known yet - and it can override any default configuration set
}

StorageFromSettings.prototype= Object.create( SeLiteData.Storage.prototype );
StorageFromSettings.prototype.constructor= StorageFromSettings;

/** @private Object serving as an associative array {
 *      string full field name: instance of StorageFromSettings
 *  }
 * */
StorageFromSettings.instances= {};

/** Create connection on demand, if it wasn't created already. See Storage.prototype.connection(). */
StorageFromSettings.prototype.connection= function connection() {
    if( !this.con ) {
        try { this.open(); }
        catch( e ) { console.debug( "Couldn't open SQLite DB " +this.parameters.fileName ); }
    }
    return this.con;
};

StorageFromSettings.prototype.open= function open() {
    if( SeLiteSettings.getTestSuiteFolder() ) {//@TODO here and below: should it just use folder-based configuration, and not default-based configuration?
        var newFileName= this.dbField.getDownToFolder().entry;
        if( newFileName ) {
            this.parameters.fileName= newFileName;
        }
    }
    else {
        var fields= this.dbField.module.getFieldsOfSet();
        if( fields[this.dbField.name] && fields[this.dbField.name].entry ) {
            this.parameters.fileName= fields[this.dbField.name].entry;
        }
    }
    SeLiteData.Storage.prototype.open.call( this );
    if( this.tablePrefixField ) {
        if( SeLiteSettings.getTestSuiteFolder() ) {
            var fieldEntry= this.tablePrefixField.getDownToFolder().entry;
            this.tablePrefixValue= fieldEntry!==undefined
                ? fieldEntry
                : '';
        }
        else {
            var fields= this.tablePrefixField.module.getFieldsOfSet();
            this.tablePrefixValue= fields[this.tablePrefixField.name] && fields[this.tablePrefixField.name].entry!==undefined
                ? fields[this.tablePrefixField.name].entry
                : '';
        }
    }
    else {
        this.tablePrefixValue= '';
    }
};

StorageFromSettings.prototype.close= function close( synchronous ) {
    SeLiteData.Storage.prototype.close.call( this, synchronous );
    this.tablePrefixValue= undefined;
    this.dbField.name in StorageFromSettings.instances || SeLiteMisc.fail( 'StorageFromSettings.close() for field ' +this.dbField.name+ " couldn't find a connection for dbField." );
};

/** @return {string} Table prefix, or an empty string. It never returns undefined.
 *  @overrides SeLiteData.Storage.tablePrefix()
 * */
StorageFromSettings.prototype.tablePrefix= function tablePrefix() {
    this.connection(); // This opens the connection, if needed, and it sets this.tablePrefixValue
    return this.tablePrefixValue;
};

/** Create a new instance of StorageFromSettings, based on the given field (or its name), or
 *  re-use an existing instance of StorageFromSettings based on that field.
 *  @param {string|SeLiteSettings.Field} appDbFieldOrFieldName Either string, or SeLiteSettings.Field.SQLite instance.
 *  If it is a string, it must be a full field name. See SeLiteSettings.getField(). Optional, it defaults to 'extensions.selite-settings.common.testDB'.
 *  @param {string|SeLiteSettings.Field} [tablePrefixFieldOrFieldName] Either string, or SeLiteSettings.Field.SQLite instance.
 *  If it is a string, it must be a full field name. See SeLiteSettings.getField(). Optional, it defaults to 'extensions.selite-settings.common.tablePrefix'. If you want to use an empty table prefix and to override any value of 'extensions.selite-settings.common.tablePrefix' field, pass an empty string '' or null.
 *  @param {boolean} [dontCreate=false] If true then this won't create a storage object,
 *  if it doesn't exist yet (then this returns null). False by default.
 *  @return {StorageFromSettings}
 */
SeLiteData.getStorageFromSettings= function getStorageFromSettings( appDbFieldOrFieldName, tablePrefixFieldOrFieldName, dontCreate=false ) {
    appDbFieldOrFieldName= appDbFieldOrFieldName!==undefined
        ? appDbFieldOrFieldName
        : 'extensions.selite-settings.common.testDB';
    var appDBfield= SeLiteSettings.getField(appDbFieldOrFieldName);
    appDBfield instanceof SeLiteSettings.Field.SQLite || SeLiteMisc.fail('Parameter appDbFieldOrFieldName must be an instance of SeLiteSettings.Field.SQLite, or a string, but it is (after processing) ' +appDbFieldOrFieldName+ '; appDBfield: ' +appDBfield );
    
    tablePrefixFieldOrFieldName= tablePrefixFieldOrFieldName!==undefined
        ? tablePrefixFieldOrFieldName
        : 'extensions.selite-settings.common.tablePrefix';
    var tablePrefixDBfield= tablePrefixFieldOrFieldName
        ? SeLiteSettings.getField(tablePrefixFieldOrFieldName)
        : undefined;
    tablePrefixFieldOrFieldName===undefined || tablePrefixDBfield instanceof SeLiteSettings.Field.String || SeLiteMisc.fail('Parameter tablePrefixDBfield must be an instance of SeLiteSettings.Field.String, or a string, but it is (after processing) ' +tablePrefixFieldOrFieldName+ '; tablePrefixField: ' +tablePrefixDBfield );
    
    SeLiteMisc.ensureType( dontCreate, 'boolean', 'dontCreate (if specified)' );
    var instance= appDBfield.name in StorageFromSettings.instances
        ? StorageFromSettings.instances[appDBfield.name]
        : (dontCreate
            ? null
            : new StorageFromSettings( appDBfield, tablePrefixDBfield )
          );
    return instance;
};

function testSuiteFolderChangeHandler() {
    Object.keys(StorageFromSettings.instances).length===0 || SeLiteSettings.getTestSuiteFolder()
    || console.log( 'SeLiteSettings: there are ' +Object.keys(StorageFromSettings.instances).length+ ' instance(s) of StorageFromSettings, yet the current test suite has no folder yet.' );
    for( var fieldName in StorageFromSettings.instances ) {
        /** @type {StorageFromSettings} */
        var instance= StorageFromSettings.instances[fieldName];
        instance instanceof StorageFromSettings || fail();
        var newFileName= instance.dbField.getDownToFolder().entry;
        if( instance.con ) {
            if( instance.parameters.fileName===newFileName ) {
                continue;
            }
            instance.close( true, function callback() {
                instance.parameters.fileName= null;
                if( newFileName ) {
                    instance.parameters.fileName= newFileName;
                    instance.open();
                }
            });
        }
        if( !newFileName ) {
            console.log( 'SeLiteSettings: The current test suite has a folder, but field ' +instance.dbField+ ' is not defined for it.' );
        }
    }
}
SeLiteSettings.addTestSuiteFolderChangeHandler( testSuiteFolderChangeHandler );

function ideCloseHandler() {
    StorageFromSettings.instances= {};
}
SeLiteSettings.addClosingIdeHandler( ideCloseHandler );

var EXPORTED_SYMBOLS= [];