diff --git a/process/SqlLib_tests/process.js b/process/SqlLib_tests/process.js index 4d7bb4e705683bf59fe1163f8c6ab5561d309c83..e3b2a0e0f3336b6f6018b642505e7eada289769a 100644 --- a/process/SqlLib_tests/process.js +++ b/process/SqlLib_tests/process.js @@ -829,7 +829,6 @@ var joinTests = new TestSuite([ .from("PERSON") .join("ORGANISATION", subQuery) - logging.log(JSON.stringify([actual._joins], null, "\t")) pTester.assert("join ORGANISATION on ORGANISATION.NAME = ?", actual._joins[0]._sqlStorage, "prepared select-sql"); pTester.assert(1, actual._joins[0].preparedValues.length, "number of params"); }], @@ -845,7 +844,6 @@ var joinTests = new TestSuite([ .from("PERSON") .join(subQuery, "orgname.NAME = TABLE2.NAME", "orgname") - logging.log(JSON.stringify([actual._joins], null, "\t")) pTester.assert("join (select NAME from ORGANISATION where ORGANISATION.NAME = ?) orgname on orgname.NAME = TABLE2.NAME", actual._joins[0]._sqlStorage, "prepared select-sql"); pTester.assert(1, actual._joins[0].preparedValues.length, "number of params"); }], @@ -857,10 +855,74 @@ var joinTests = new TestSuite([ .from("PERSON") .join("TABLE1 on TABLE1.NAME = TABLE2.NAME") - logging.log(JSON.stringify([actual._joins], null, "\t")) pTester.assert("join TABLE1 on TABLE1.NAME = TABLE2.NAME", actual._joins[0]._sqlStorage, "prepared select-sql"); pTester.assert(0, actual._joins[0].preparedValues.length, "number of params"); }] +]); + +var subqueryAsFieldTests = new TestSuite([ + ["Test if a Subselect as field works if pValue is provided. This should be added as subselect.", function(pTester) + { + var subQuery = newSelect("NAME") + .from("ORGANISATION") + .where("ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID") + .and("PERSON.FIRSTNAME", "val1") // test if the value is added at the correct place + + var actual = new SqlBuilder().where(subQuery, "val2", "# = ?", SQLTYPES.VARCHAR) + .and("PERSON.FIRSTNAME", "val3"); + + + pTester.assert(" ( ( select NAME from ORGANISATION where ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID and PERSON.FIRSTNAME = ? ) = ? ) and PERSON.FIRSTNAME = ?", actual._where._sqlStorage, "prepared select-sql"); + pTester.assert(3, actual._where.preparedValues.length, "number of params"); + pTester.assert("val1", actual._where.preparedValues[0][0], "param 1 is correct value"); + pTester.assert("val2", actual._where.preparedValues[1][0], "param 2 is correct value"); + pTester.assert("val3", actual._where.preparedValues[2][0], "param 3 is correct value"); + }], + + ["Test if a Subselect as field should error if no SQLTYPE is provided.", function(pTester) + { + var subQuery = newSelect("NAME") + .from("ORGANISATION") + .where("ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID") + .and("PERSON.FIRSTNAME", "val1") // test if the value is added at the correct place + new SqlBuilder().where(subQuery, "val2", "# = ?"); + }, SqlBuilder.ERROR_SUBSELECT_AS_FIELD_NO_FIELD_TYPE()], + + ["Test if a Subselect as field should error if it is not a full select.", function(pTester) + { + var subQuery = newSelect("NAME") + .where("ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID") + .and("PERSON.FIRSTNAME", "val1") // test if the value is added at the correct place + + new SqlBuilder().where(subQuery, "val2", "# = ?", SQLTYPES.VARCHAR); + }, SqlBuilder.ERROR_SUBSELECT_AS_FIELD_NOT_COMPLETE()] +]); + +var conditionFormatTests = new TestSuite([ + ["pCondition should not fail if # an ? exist in correct order", function(pTester) + { + new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "# = ?") + }], + + ["pCondition should not fail if only ? exists", function(pTester) + { + new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "?") + }], + + ["pCondition should fail if more than one ? exists", function(pTester) + { + new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "? test ?") + }, SqlBuilder.ERROR_CONDITION_WRONG_FORMAT()], + + ["pCondition should fail if more than one # exists", function(pTester) + { + new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "# test #") + }, SqlBuilder.ERROR_CONDITION_WRONG_FORMAT()], + + ["pCondition should fail if # and ? are in wrong order", function(pTester) + { + new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "? = #") + }, SqlBuilder.ERROR_CONDITION_WRONG_FORMAT()] ]) var tester = new Tester("Test SqlBuilder"); @@ -875,6 +937,8 @@ tester.test(inStatementTests); tester.test(testConstantFunctions); tester.test(selectTests); tester.test(joinTests); +tester.test(subqueryAsFieldTests); +tester.test(conditionFormatTests); logging.log("-------------------------"); tester.printResults(); diff --git a/process/Sql_lib/process.js b/process/Sql_lib/process.js index 3b83a1f8c106405d262bbeec90f8008b22815080..cf6c9feef16636a0a84e949895a170b5e7202f78 100644 --- a/process/Sql_lib/process.js +++ b/process/Sql_lib/process.js @@ -39,6 +39,7 @@ function newSelect(pFields, pAlias) * @param {String} [pCondition="# = ?"] This is the condition which should be used.<br/> * # will be replaced by the field (pFieldOrCond) If pFieldOrCond is null, you can ommit #<br/> * ? will be replaced by pValue<br/> + * <strong>IMPORTANT: the # has to be before the ?</strong><br/> * Please see .where() for more information and examples. * @param {SQLTYPES|Numeric} [pFieldType=AutomaticallyLoadedType] You can specify which datatype should be used for the prepared statement<br/> * In most cases you don't need this.<br/> @@ -65,6 +66,7 @@ function newWhere(pFieldOrCond, pValue, pCondition, pFieldType, pAlias) * @param {String} [pCondition="# = ?"] This is the condition which should be used.<br/> * # will be replaced by the field (pFieldOrCond) If pFieldOrCond is null, you can ommit #<br/> * ? will be replaced by pValue<br/> + * <strong>IMPORTANT: the # has to be before the ?</strong><br/> * Please see .whereIfSet() for more information and examples. * @param {SQLTYPES|Numeric} [pFieldType=AutomaticallyLoadedType] You can specify which datatype should be used for the prepared statement<br/> * In most cases you don't need this.<br/> @@ -106,6 +108,8 @@ function SqlBuilder (pAlias) this._unions = []; this.alias = pAlias; + this._subselectAlias = null; + this._where = { preparedValues: [], _sqlStorage: "", @@ -156,32 +160,32 @@ SqlBuilder.ERROR_INSTANCIATE_WITH_NEW = function() SqlBuilder.ERROR_INVALID_CONDITION_VALUE_TYPE = function() { - return new Error(translate.text("SqlBuilder.and/or: invalid value-type for pCondition")); + return new Error(translate.text("SqlBuilder: invalid value-type for pCondition")); } SqlBuilder.ERROR_NO_CONDITION = function() { - return new Error(translate.text("SqlBuilder.and/or: if you use a subQuery (e.g. SqlBuilder) you have to provide pCondition (e.g. \"exists ?\")")); + return new Error(translate.text("SqlBuilder: if you use a subQuery (e.g. SqlBuilder) you have to provide pCondition (e.g. \"exists ?\")")); } SqlBuilder.ERROR_INVALID_SUBQUERY_TYPE = function() { - return new Error(translate.text("SqlBuilder.and/or: invalid value-type for pFieldOrCond. It can be a fully qualified SqlBuilder (e.g. select, from, ... have to be set) or an jdito-prepared-statement array")); + return new Error(translate.text("SqlBuilder: invalid value-type for pFieldOrCond. It can be a fully qualified SqlBuilder (e.g. select, from, ... have to be set) or an jdito-prepared-statement array")); } SqlBuilder.ERROR_VALUE_IS_MANDATORY = function() { - return new Error(translate.text("SqlBuilder.and/or: pValue (or pFieldOrCond if only one param) is not allowed to be null, undefined or []. (use *IfSet functions if you need optional conditions which are just ignored if value is null or undefined)")); + return new Error(translate.text("SqlBuilder: pValue (or pFieldOrCond if only one param) is not allowed to be null, undefined or []. (use *IfSet functions if you need optional conditions which are just ignored if value is null or undefined)")); } SqlBuilder.ERROR_VALUE_IS_MANDATORY_JDITO_VAR = function() { - return new Error(translate.text("SqlBuilder.and/or: pValue has to be a jdito variable which returns something different than null. (use *IfSet functions if you need optional conditions which are just ignored if value is null or undefined)")); + return new Error(translate.text("SqlBuilder: pValue has to be a jdito variable which returns something different than null. (use *IfSet functions if you need optional conditions which are just ignored if value is null or undefined)")); } SqlBuilder.ERROR_UNSUPPORTED_PARAMETER_COMBINATION = function() { - return new Error(translate.text("SqlBuilder.and/or: unsupportet parameter combination")); + return new Error(translate.text("SqlBuilder: unsupportet parameter combination")); } SqlBuilder.ERROR_NO_TABLE = function() @@ -191,7 +195,7 @@ SqlBuilder.ERROR_NO_TABLE = function() SqlBuilder.ERROR_NO_PARAMETER_PROVIDED = function() { - return new Error(translate.text("SqlBuilder.and/or: You have to specify at least one parameter")); + return new Error(translate.text("SqlBuilder: You have to specify at least one parameter")); } SqlBuilder.ERROR_WHERE_NOT_FIRST = function() @@ -211,9 +215,25 @@ SqlBuilder.ERROR_INCOMPLETE_SELECT = function () SqlBuilder.ERROR_CONDITION_IS_MANDATORY = function () { - return new Error(translate.text("SqlBuilder.and/or: You have to provide a subquery as SqlBuilder, prepared-array or string")); + return new Error(translate.text("SqlBuilder: You have to provide a subquery as SqlBuilder, prepared-array or string")); +} + +SqlBuilder.ERROR_SUBSELECT_AS_FIELD_NOT_COMPLETE = function () +{ + return new Error(translate.text("SqlBuilder: If pFieldOrCond is a SqlBuilder & pValue is provided, pFieldOrCond has to be a full SqlBuilder which will be used as subselect")); +} + +SqlBuilder.ERROR_SUBSELECT_AS_FIELD_NO_FIELD_TYPE = function () +{ + return new Error(translate.text("SqlBuilder: If pFieldOrCond is a SqlBuilder & pValue is provided, you have to provide also pFieldType, as the type cannot be calculated from pFieldOrCond because it is a subselect")); } +SqlBuilder.ERROR_CONDITION_WRONG_FORMAT = function () +{ + return new Error(translate.text("SqlBuilder: The '#' in pCondition has to occur before the '?' and '?' has to occur 1 time, '#' has to occur 1 or 0 times.")); +} + + /** * Builds the sql and uses SqlUtils.translateXXXWithQuotes to make a string out of it. * @return {String} the sql as string @@ -248,6 +268,18 @@ SqlBuilder.prototype.select = function(pFields) return this; } +/** + * sets an alias-name which is added to the built sql if it is a full select + * @param {String} pSubselectAlias + * + * @return {SqlBuilder} current SqlBuilder object + */ +SqlBuilder.prototype.subselectAlias = function(pSubselectAlias) +{ + this._subselectAlias = pSubselectAlias; + return this; +} + /** * Sets the from clause of the sql.<br/> * <br/> @@ -381,8 +413,8 @@ SqlBuilder.prototype.rightJoin = function(pTable, pCondition, pTableAlias) * * @param {String} [pCondition="# = ?"] This is the condition which should be used to compare the field with the value.<br/> * # will be replaced by the field (pFieldOrCond) If pFieldOrCond is null, you can ommit #<br/> - * ? will be replaced by pValue - * + * ? will be replaced by pValue<br/> + * <strong>IMPORTANT: the # has to be before the ?</strong> * @param {SQLTYPES|Numeric} [pFieldType=AutomaticallyLoadedType] You can specify which datatype should be used for the prepared statement <br/> * In most cases you don't need this.<br/> * This is helpful if you for example have a pCondition "year(#) = ?"<br/> @@ -431,6 +463,7 @@ SqlBuilder.prototype.where = function(pFieldOrCond, pValue, pCondition, pFieldTy * @param {String} [pCondition="# = ?"] This is the condition which should be used to compare the field with the value.<br/> * # will be replaced by the field (pFieldOrCond) If pFieldOrCond is null, you can ommit #<br/> * ? will be replaced by pValue<br/> + * <strong>IMPORTANT: the # has to be before the ?</strong> * * @param {SQLTYPES|Numeric} [pFieldType=AutomaticallyLoadedType] You can specify which datatype should be used for the prepared statement <br/> * In most cases you don't need this.<br/> @@ -450,7 +483,7 @@ SqlBuilder.prototype.whereIfSet = function(pFieldOrCond, pValue, pCondition, pFi * * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] - * @param {String} [pCondition="# = ?"] + * @param {String} [pCondition="# = ?"] <strong>IMPORTANT: the # has to be before the ?</strong><br/> * @param {SQLTYPES|Numeric} [pFieldType=AutomaticallyLoadedType] * @param {SQLTYPES|Numeric} pAddCondFn=AutomaticallyLoadedType This is a callback which is called if a condition should be added (needs to have same parameters as .or() * @@ -503,7 +536,7 @@ SqlBuilder.prototype._whereCondition = function(pCondition, pMandatory, pAddPrep // the field is a simple string -> just add the string, no prepared statement if (typeofSql == "string") { - pAddPreparedConditionCallback([sql, []]); + pAddPreparedConditionCallback(this, [sql, []]); return this; } @@ -518,7 +551,7 @@ SqlBuilder.prototype._whereCondition = function(pCondition, pMandatory, pAddPrep if (pBrackets) sql[0] = " ( " + sql[0] + " ) "; - pAddPreparedConditionCallback([sql[0], []], pBrackets) + pAddPreparedConditionCallback(this, [sql[0], []], pBrackets) return this; } else if (pMandatory) @@ -540,7 +573,7 @@ SqlBuilder.prototype._whereCondition = function(pCondition, pMandatory, pAddPrep if (pBrackets) condString = " ( " + condString + " ) "; - pAddPreparedConditionCallback([condString, sql._where.preparedValues], pBrackets); + pAddPreparedConditionCallback(this, [condString, sql._where.preparedValues], pBrackets); return this; } else if (pMandatory) @@ -578,7 +611,7 @@ SqlBuilder.prototype._whereSubquery = function(pSubquery, pMandatory, pCondition if (Array.isArray(sql)) { if (sql[0]) - pAddPreparedConditionCallback(this._prepare(undefined, sql, pCondition)); + pAddPreparedConditionCallback(this, this._prepare(undefined, sql, pCondition)); else if (pMandatory) throw SqlBuilder.ERROR_VALUE_IS_MANDATORY(); @@ -597,7 +630,7 @@ SqlBuilder.prototype._whereSubquery = function(pSubquery, pMandatory, pCondition if (subQuery.isFullSelect()) { var preparedObj = subQuery.build(); - pAddPreparedConditionCallback(this._prepare(undefined, preparedObj, pCondition)); + pAddPreparedConditionCallback(this, this._prepare(undefined, preparedObj, pCondition)); } else if (pMandatory) throw SqlBuilder.ERROR_VALUE_IS_MANDATORY(); @@ -623,6 +656,12 @@ SqlBuilder.prototype._whereSubquery = function(pSubquery, pMandatory, pCondition */ SqlBuilder.prototype._addWhere = function(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType, pAddPreparedConditionCallback) { + if (pCondition && !SqlUtils.checkConditionFormat(pCondition)) + throw SqlBuilder.ERROR_CONDITION_WRONG_FORMAT(); + + if (pMandatory === undefined) + pMandatory = true; + if (!this._where._whereWasCalled) throw SqlBuilder.ERROR_WHERE_NOT_FIRST(); @@ -644,21 +683,40 @@ SqlBuilder.prototype._addWhere = function(pFieldOrCond, pValue, pMandatory, pCon else return this; } - - if (pMandatory === undefined) - pMandatory = true; // just call the andCondition function if it is only a Condition if (pFieldOrCond !== undefined && pValue === undefined && pCondition === undefined && pFieldType === undefined) return this._whereCondition(pFieldOrCond, pMandatory, pAddPreparedConditionCallback, true); + if (pFieldOrCond instanceof SqlBuilder) + { + if (!pFieldOrCond.isFullSelect()) + { + throw SqlBuilder.ERROR_SUBSELECT_AS_FIELD_NOT_COMPLETE(); + } + + if (!pFieldType) + throw SqlBuilder.ERROR_SUBSELECT_AS_FIELD_NO_FIELD_TYPE(); + + var tmpCond = newWhere(this.alias) + ._addWhere("SQL_LIB_DUMMY_TABLE.SQL_LIB_DUMMY_COLUMN", pValue, pMandatory, pCondition, pFieldType, pAddPreparedConditionCallback); + + var subSqlPrepared = pFieldOrCond.build(); + + tmpCond._where._sqlStorage = SqlUtils.replaceConditionTemplate(tmpCond._where._sqlStorage, 'SQL_LIB_DUMMY_TABLE.SQL_LIB_DUMMY_COLUMN', "( " + subSqlPrepared[0] + " )"); + tmpCond._where.preparedValues = subSqlPrepared[1].concat(tmpCond._where.preparedValues) + + this._whereCondition(tmpCond, pMandatory, pAddPreparedConditionCallback, true) + return this; + } + // first check the default-mandatory-cases: null or undefined. everything else such as checking $-variables is done later if (pMandatory && (pValue === null || pValue === undefined)) throw SqlBuilder.ERROR_VALUE_IS_MANDATORY(); // a field is string or array -> normal case // OR !pFieldOrCond and pValue and pCondition is given -> preparedSQL/SqlBuilder can be used without field if pCondition is set - if((typeof pFieldOrCond == "string" || Array.isArray(pFieldOrCond)) || (!pFieldOrCond && (pCondition && pValue instanceof SqlBuilder || !(pValue instanceof SqlBuilder)))) + if(((typeof pFieldOrCond == "string" || Array.isArray(pFieldOrCond) || (!pFieldOrCond && pFieldType)) || (!pFieldOrCond && (pCondition && pValue instanceof SqlBuilder || !(pValue instanceof SqlBuilder))))) { var field = pFieldOrCond; var typeofValue = typeof pValue; @@ -681,6 +739,7 @@ SqlBuilder.prototype._addWhere = function(pFieldOrCond, pValue, pMandatory, pCon if (!pCondition) pCondition = "# in ?"; + // if it is null -> ignore it. -> the pCondition should not contain a # in this case if (field != null) { [alias, parsedField] = SqlUtils.parseField(field) @@ -733,11 +792,13 @@ SqlBuilder.prototype._addWhere = function(pFieldOrCond, pValue, pMandatory, pCon // ... everything else -> just pass it if (pValue === false || pValue === 0 || pValue === "" || pValue) { - this._whereCondition(this._prepare(field, pValue, pCondition, pFieldType), undefined, pAddPreparedConditionCallback); + let prep = this._prepare(field, pValue, pCondition, pFieldType) + this._whereCondition(prep, undefined, pAddPreparedConditionCallback); } return this; } + throw SqlBuilder.ERROR_UNSUPPORTED_PARAMETER_COMBINATION(); } @@ -755,9 +816,7 @@ SqlBuilder.prototype._addWhere = function(pFieldOrCond, pValue, pMandatory, pCon */ SqlBuilder.prototype._and = function(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType) { - var that = this; - - return this._addWhere(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType, function(pPreparedCondition) + return this._addWhere(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType, function(that, pPreparedCondition) { that._where._previouslyOnlyOr = false; if (pPreparedCondition.length == 2 && typeof pPreparedCondition[0] == "string" && pPreparedCondition[0] != "" && Array.isArray(pPreparedCondition[1])) @@ -784,10 +843,8 @@ SqlBuilder.prototype._and = function(pFieldOrCond, pValue, pMandatory, pConditio * @ignore */ SqlBuilder.prototype._or = function(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType) -{ - var that = this; - - return this._addWhere(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType, function(pPreparedCondition, pAlreadySurroundedByBrackets) +{ + return this._addWhere(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType, function(that, pPreparedCondition, pAlreadySurroundedByBrackets) { if (pPreparedCondition.length == 2 && typeof pPreparedCondition[0] == "string" && pPreparedCondition[0] != "" && Array.isArray(pPreparedCondition[1])) { @@ -1306,9 +1363,15 @@ SqlBuilder.prototype.buildCondition = function() SqlBuilder.prototype.build = function(pDefaultConditionIfNone) { var wherePrefix = ""; + var subselectAlias = ""; - if (this.isFullSelect() && this._where._sqlStorage) - wherePrefix = "where "; + if (this.isFullSelect()) + { + if (this._where._sqlStorage) + wherePrefix = "where "; + + subselectAlias = this._subselectAlias; + } var whereSql = this._where._sqlStorage; @@ -1344,6 +1407,8 @@ SqlBuilder.prototype.build = function(pDefaultConditionIfNone) preparedVals = preparedVals.concat(part.preparedValues); } } + + sqlStr += (subselectAlias ? " " + subselectAlias : ""); return [sqlStr, preparedVals]; } @@ -2683,4 +2748,17 @@ SqlUtils.replaceConditionTemplate = function(pCondition, pPlaceholder, pReplacem replacements["\\\\" + pPlaceholder] = pPlaceholder return text.replaceAll(pCondition, replacements); +} + +SqlUtils.checkConditionFormat = function(pCondition) +{ + // replace by {@NUMBERSIGN@} / {@QUESTIONSIGN@} as the js-regexp cannot do lookbehind (needed by the regexp used in replaceConditionTemplate to check escapes) + // so we just use replaceConditionTemplate to replace by something which never should occur anywhere + pCondition = SqlUtils.replaceConditionTemplate(pCondition, "#", "{@NUMBERSIGN@}") + pCondition = SqlUtils.replaceConditionTemplate(pCondition, "\\?", "{@QUESTIONSIGN@}") + + var indexOfNumberSign = pCondition.indexOf("{@NUMBERSIGN@}"); + var indexOfQuestionSign = pCondition.indexOf("{@QUESTIONSIGN@}"); + + return !(indexOfQuestionSign == -1 || indexOfNumberSign > indexOfQuestionSign || indexOfNumberSign != pCondition.lastIndexOf("{@NUMBERSIGN@}") || indexOfQuestionSign != pCondition.lastIndexOf("{@QUESTIONSIGN@}")) } \ No newline at end of file