From 4fc6c4dc02a627b583dd4faf45f7307674b530ef Mon Sep 17 00:00:00 2001 From: Sebastian Listl <s.listl@adito.de> Date: Mon, 25 Oct 2021 19:27:09 +0200 Subject: [PATCH] 1086863 SqlBuilder IN condition with over 1000 values --- process/ArrayUtils_test/ArrayUtils_test.aod | 12 + process/ArrayUtils_test/process.js | 52 + process/JditoFilter_lib/process.js | 97 +- process/SqlBuilder_test/process.js | 154 +- process/Sql_lib/process.js | 7569 ++++++++++--------- process/Util_lib/process.js | 27 + 6 files changed, 4042 insertions(+), 3869 deletions(-) create mode 100644 process/ArrayUtils_test/ArrayUtils_test.aod create mode 100644 process/ArrayUtils_test/process.js diff --git a/process/ArrayUtils_test/ArrayUtils_test.aod b/process/ArrayUtils_test/ArrayUtils_test.aod new file mode 100644 index 0000000000..ccb19f2cc7 --- /dev/null +++ b/process/ArrayUtils_test/ArrayUtils_test.aod @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.2" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.2"> + <name>ArrayUtils_test</name> + <title>[TEST] Util_lib - ArrayUtils</title> + <majorModelMode>DISTRIBUTED</majorModelMode> + <icon>VAADIN:CHECK_CIRCLE</icon> + <process>%aditoprj%/process/ArrayUtils_test/process.js</process> + <alias>Data_alias</alias> + <variants> + <element>EXECUTABLE</element> + </variants> +</process> diff --git a/process/ArrayUtils_test/process.js b/process/ArrayUtils_test/process.js new file mode 100644 index 0000000000..04c1f4e13e --- /dev/null +++ b/process/ArrayUtils_test/process.js @@ -0,0 +1,52 @@ +import("Util_lib"); +import("system.result"); +import("system.vars"); +import("UnitTest_lib"); + +var chunk = new TestSuite("ArrayUtils.chunk", [ + new Test("should test if an arrays are split up into the correct amount of chunks", + function (pTester, pDataProvider) + { + var chunkSize = pDataProvider[1]; + var chunkedArray = ArrayUtils.chunk(pDataProvider[0], chunkSize); + var expectedChunkCount = pDataProvider[2]; + + pTester.expectThat(chunkedArray).hasLength(expectedChunkCount).assert(); + }, + function dataProvider() + { + return [ + [["a","b","c","d","e","f","g","h","i","j"], 2, 5], //exactly divisible + [["k","o","n","o","d","i","o","d","a","o"], 3, 4], //last chunk not full + [["a","b","c","d","e","f","g","h","i","j"], 16, 1], //first chunk not full + [["a","b","c","d","e","n","a","n","i","o"], 0, 0] //chunksize 0 => empty array + ]; + } + ), + new Test("should test if the size of the chunks is correct", + function (pTester, pDataProvider) + { + var chunkSize = pDataProvider[1]; + var chunkedArray = ArrayUtils.chunk(pDataProvider[0], chunkSize); + + pTester.expectThat(chunkedArray).elementAt(0).hasLength(chunkSize).assert(); + pTester.expectThat(chunkedArray).elementAt(-1).hasMaxLength(chunkSize).assert(); + }, + function dataProvider() + { + return [ + [["a","b","a","k","a","y","a","r","o","u"], 2], //exactly divisible + [["a","b","c","d","e","f","g","h","i","j"], 3] //last chunk not full + ]; + } + ) +]); + + +var tester = new Tester("Test ArrayUtils"); +tester.initCoverage(ArrayUtils); +tester.test(chunk); + +tester.summary(); + +result.object(tester.getResults()); diff --git a/process/JditoFilter_lib/process.js b/process/JditoFilter_lib/process.js index 29b6ee9e6a..d6292dc2df 100644 --- a/process/JditoFilter_lib/process.js +++ b/process/JditoFilter_lib/process.js @@ -5,6 +5,7 @@ import("system.logging"); import("Sql_lib"); import("Util_lib"); import("system.datetime"); +import("system.neonFilter"); //@TODO: add support for permissions to the lib @@ -18,7 +19,7 @@ function FilterConditionGroup (pGroup) if (pGroup && "filter" in pGroup) pGroup = pGroup.filter; - var operator = pGroup ? pGroup.operator : "AND"; + var operator = pGroup ? pGroup.operator : neonFilter.MERGE_OPERATOR_AND; var childs = pGroup ? pGroup.childs : []; this.type = "group"; this.operator = operator; @@ -229,7 +230,7 @@ JditoFilter.prototype.lookupFilterFields = function (pFields) this._lookupFields = pFields; var lookupFilterFn = function (pRecordValue, pFilterValue, pOperator, pRow) { - if (pOperator == "CONTAINS") + if (pOperator == neonFilter.SEARCH_OPERATOR_CONTAINS) { var filterValues = pFilterValue.split(" ").filter(function (val) {return val.trim();}); return filterValues.every(function (filterValue) @@ -290,7 +291,7 @@ JditoFilter.prototype.checkRecord = function (pRow) } else if (pCondition.type == "group") { - if (pCondition.operator == "AND") + if (pCondition.operator == neonFilter.MERGE_OPERATOR_AND) return pCondition.childs.every(_testRecord, this); return pCondition.childs.some(_testRecord, this); } @@ -304,34 +305,34 @@ JditoFilter.prototype.checkRecord = function (pRow) { switch (pOperator) { - case "CONTAINS": + case neonFilter.SEARCH_OPERATOR_CONTAINS: return (new RegExp(RegExpUtils.escapePatternStr(pFilterValue), regexFlags)).test(pRowValue); - case "CONTAINSNOT": + case neonFilter.SEARCH_OPERATOR_CONTAINSNOT: return !(new RegExp(RegExpUtils.escapePatternStr(pFilterValue), regexFlags)).test(pRowValue); - case "STARTSWITH": + case neonFilter.SEARCH_OPERATOR_STARTSWITH: return (new RegExp("^" + RegExpUtils.escapePatternStr(pFilterValue), regexFlags)).test(pRowValue); - case "ENDSWITH": + case neonFilter.SEARCH_OPERATOR_ENDSWITH: return (new RegExp(RegExpUtils.escapePatternStr(pFilterValue) + "$", regexFlags)).test(pRowValue); - case "EQUAL": + case neonFilter.SEARCH_OPERATOR_EQUAL: return (new RegExp("^" + RegExpUtils.escapePatternStr(pFilterValue) + "$", regexFlags)).test(pRowValue); - case "NOT_EQUAL": + case neonFilter.SEARCH_OPERATOR_NOT_EQUAL: return !(new RegExp("^" + RegExpUtils.escapePatternStr(pFilterValue) + "$", regexFlags)).test(pRowValue); //String-comparison returns false values < <= > >= are just relevant for numbervalues - case "LESS": + case neonFilter.SEARCH_OPERATOR_LESS: return new Number(pRowValue) < new Number(pFilterValue); - case "LESS_OR_EQUAL": + case neonFilter.SEARCH_OPERATOR_LESS_OR_EQUAL: return new Number(pRowValue) <= new Number(pFilterValue); - case "GREATER": + case neonFilter.SEARCH_OPERATOR_GREATER: return new Number(pRowValue) > new Number(pFilterValue); - case "GREATER_OR_EQUAL": + case neonFilter.SEARCH_OPERATOR_GREATER_OR_EQUAL: return new Number(pRowValue) >= new Number(pFilterValue); - case "ISNULL": + case neonFilter.SEARCH_OPERATOR_ISNULL: return pRowValue == ""; - case "ISNOTNULL": + case neonFilter.SEARCH_OPERATOR_ISNOTNULL: return pRowValue != ""; - case "TIMEFRAME_EQUAL": - case "TIMEFRAME_COMING": - case "TIMEFRAME_PAST": + case neonFilter.SEARCH_OPERATOR_TIMEFRAME_EQUAL: + case neonFilter.SEARCH_OPERATOR_TIMEFRAME_COMING: + case neonFilter.SEARCH_OPERATOR_TIMEFRAME_PAST: var [start, end] = datetime.resolveRelativeDateExpression(pFilterValue, new Date().getTime(), vars.get("$sys.timezone")); return pRowValue >= start && pRowValue <= end; } @@ -506,7 +507,7 @@ JditoFilterUtils.getEmptyFilter = function (pEntity) entity: pEntity, filter: { type: "group", - operator: "AND", + operator: neonFilter.MERGE_OPERATOR_AND, childs: [] } }; @@ -698,9 +699,9 @@ FilterSqlTranslator.prototype.getSqlCondition = function () var conditionFn = fieldConditionFns[pFilter.name]; condition = conditionFn.call(null, filterValue, pFilter.operator); - if (pOperator == "AND") + if (pOperator == neonFilter.MERGE_OPERATOR_AND) this.andIfSet(condition); - else if (pOperator == "OR") + else if (pOperator == neonFilter.MERGE_OPERATOR_OR) this.orIfSet(condition); return; @@ -722,23 +723,23 @@ FilterSqlTranslator.prototype.getSqlCondition = function () var generatedCondition = _getCondition(filterValue, pFilter.operator, sqlField, pFilter.contenttype); if (generatedCondition instanceof SqlBuilder || Utils.isString(generatedCondition)) { - if (pOperator == "AND") + if (pOperator == neonFilter.MERGE_OPERATOR_AND) this.andIfSet(generatedCondition); - else if (pOperator == "OR") + else if (pOperator == neonFilter.MERGE_OPERATOR_OR) this.orIfSet(generatedCondition); } else { - var isStringType = pFilter.contenttype != "NUMBER" - && pFilter.contenttype != "DATE" - && pFilter.contenttype != "BOOLEAN"; + var isStringType = pFilter.contenttype != neonFilter.CONTENT_TYPE_NUMBER + && pFilter.contenttype != neonFilter.CONTENT_TYPE_DATE + && pFilter.contenttype != neonFilter.CONTENT_TYPE_BOOLEAN; [condition, filterValue] = generatedCondition; if (isStringType && ignoreCase) condition = condition.replace("#", "UPPER(#)").replace("?", "UPPER(?)"); - if (pOperator == "AND") + if (pOperator == neonFilter.MERGE_OPERATOR_AND) this.andIfSet(sqlField, filterValue, condition); - else if (pOperator == "OR") + else if (pOperator == neonFilter.MERGE_OPERATOR_OR) this.orIfSet(sqlField, filterValue, condition); } } @@ -750,16 +751,16 @@ FilterSqlTranslator.prototype.getSqlCondition = function () { _addCondition.call(subCondition, cond, operator); }); - if (pOperator == "AND") + if (pOperator == neonFilter.MERGE_OPERATOR_AND) this.andIfSet(subCondition); - else if (pOperator == "OR") + else if (pOperator == neonFilter.MERGE_OPERATOR_OR) this.orIfSet(subCondition); } } function _handleDate(pValue, pType) { - if(pValue && pType == "DATE") + if(pValue && pType == neonFilter.CONTENT_TYPE_DATE) { pValue = datetime.switchTimezone(pValue, null, vars.get("$sys.timezone")); } @@ -772,16 +773,16 @@ FilterSqlTranslator.prototype.getSqlCondition = function () { switch (pOperator) { - case "CONTAINS": + case neonFilter.SEARCH_OPERATOR_CONTAINS: return [SqlBuilder.LIKE(), "%" + pValue + "%"]; - case "CONTAINSNOT": + case neonFilter.SEARCH_OPERATOR_CONTAINSNOT: return [SqlBuilder.NOT_LIKE(), "%" + pValue + "%"]; - case "STARTSWITH": + case neonFilter.SEARCH_OPERATOR_STARTSWITH: return [SqlBuilder.LIKE(), pValue + "%"]; - case "ENDSWITH": + case neonFilter.SEARCH_OPERATOR_ENDSWITH: return [SqlBuilder.LIKE(), "%" + pValue]; - case "EQUAL": - if(pType == "DATE") + case neonFilter.SEARCH_OPERATOR_EQUAL: + if(pType == neonFilter.CONTENT_TYPE_DATE) { let start = _handleDate(pValue, pType); let end = new Date(start); @@ -795,8 +796,8 @@ FilterSqlTranslator.prototype.getSqlCondition = function () { return [SqlBuilder.EQUAL(), pValue]; } - case "NOT_EQUAL": - if(pType == "DATE") + case neonFilter.SEARCH_OPERATOR_NOT_EQUAL: + if(pType == neonFilter.CONTENT_TYPE_DATE) { let start = _handleDate(pValue, pType); let end = new Date(start); @@ -810,25 +811,25 @@ FilterSqlTranslator.prototype.getSqlCondition = function () { return [SqlBuilder.NOT_EQUAL(), pValue]; } - case "LESS": + case neonFilter.SEARCH_OPERATOR_LESS: pValue = _handleDate(pValue, pType); return [SqlBuilder.LESS(), pValue]; - case "LESS_OR_EQUAL": + case neonFilter.SEARCH_OPERATOR_LESS_OR_EQUAL: pValue = _handleDate(pValue, pType); return [SqlBuilder.LESS_OR_EQUAL(), pValue]; - case "GREATER": + case neonFilter.SEARCH_OPERATOR_GREATER: pValue = _handleDate(pValue, pType); return [SqlBuilder.GREATER(), pValue]; - case "GREATER_OR_EQUAL": + case neonFilter.SEARCH_OPERATOR_GREATER_OR_EQUAL: pValue = _handleDate(pValue, pType); return [SqlBuilder.GREATER_OR_EQUAL(), pValue]; - case "ISNULL": + case neonFilter.SEARCH_OPERATOR_ISNULL: return pField + " is null"; - case "ISNOTNULL": + case neonFilter.SEARCH_OPERATOR_ISNOTNULL: return pField + " is not null"; - case "TIMEFRAME_EQUAL": - case "TIMEFRAME_COMING": - case "TIMEFRAME_PAST": + case neonFilter.SEARCH_OPERATOR_TIMEFRAME_EQUAL: + case neonFilter.SEARCH_OPERATOR_TIMEFRAME_COMING: + case neonFilter.SEARCH_OPERATOR_TIMEFRAME_PAST: var [start, end] = datetime.resolveRelativeDateExpression(pValue, new Date().getTime(), vars.get("$sys.timezone")); return newWhere(pField, start, SqlBuilder.GREATER_OR_EQUAL()) .and(pField, end, SqlBuilder.LESS_OR_EQUAL()); diff --git a/process/SqlBuilder_test/process.js b/process/SqlBuilder_test/process.js index 3ec9ec86dd..af79070862 100644 --- a/process/SqlBuilder_test/process.js +++ b/process/SqlBuilder_test/process.js @@ -14,7 +14,7 @@ var newSelectTests = new TestSuite("SqlLib.newSelect", [ { var actualValue = newSelect("MySuper, Field, String") - pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select MySuper, Field, String").assert(); + pTester.expectThat(actualValue).elementAt("_select").elementAt("sqlStorage").equals("select MySuper, Field, String").assert(); pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(0).assert(); } ), @@ -24,7 +24,7 @@ var newSelectTests = new TestSuite("SqlLib.newSelect", [ { var actualValue = newSelect(["MySuper", "Field", "String"]) - pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select MySuper, Field, String").assert(); + pTester.expectThat(actualValue).elementAt("_select").elementAt("sqlStorage").equals("select MySuper, Field, String").assert(); pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(0).assert(); } ), @@ -34,7 +34,7 @@ var newSelectTests = new TestSuite("SqlLib.newSelect", [ { var actualValue = newSelect(new SqlBuilder().select("PERSONID").from("PERSON").where("PERSON.FIRSTNAME", "Fritz")) - pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select (select PERSONID from PERSON where PERSON.FIRSTNAME = ?)").assert(); + pTester.expectThat(actualValue).elementAt("_select").elementAt("sqlStorage").equals("select (select PERSONID from PERSON where PERSON.FIRSTNAME = ?)").assert(); pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -44,7 +44,7 @@ var newSelectTests = new TestSuite("SqlLib.newSelect", [ { var actualValue = newSelect(["MySuper", "Field", "String", new SqlBuilder().select("PERSONID").from("PERSON").where("PERSON.FIRSTNAME", "Fritz")]) - pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select MySuper, Field, String, (select PERSONID from PERSON where PERSON.FIRSTNAME = ?)").assert(); + pTester.expectThat(actualValue).elementAt("_select").elementAt("sqlStorage").equals("select MySuper, Field, String, (select PERSONID from PERSON where PERSON.FIRSTNAME = ?)").assert(); pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -59,7 +59,7 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ .where("PERSON.FIRSTNAME = 'Tim'") // NOTE: you should not do this as this does not add a real prepared statement with "?" .and("PERSON.LASTNAME = 'Admin'") - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = 'Tim' and PERSON.LASTNAME = 'Admin'").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("PERSON.FIRSTNAME = 'Tim' and PERSON.LASTNAME = 'Admin'").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0).assert(); } ), @@ -71,7 +71,7 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ .where("PERSON.FIRSTNAME", "Tim") .and("PERSON.LASTNAME", "Admin") - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -83,7 +83,7 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ .where("PERSON.FIRSTNAME", "") .and("PERSON.LASTNAME", "") - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -117,7 +117,7 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ .where("PERSON.FIRSTNAME", "$global.TestingVarEmptyString") .and("PERSON.LASTNAME", "$global.TestingVarEmptyString") - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -129,7 +129,7 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ .where("PERSON.FIRSTNAME", "Tim", "# <> ?") .and("PERSON.LASTNAME", "Admin") - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME <> ? and PERSON.LASTNAME = ?").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("PERSON.FIRSTNAME <> ? and PERSON.LASTNAME = ?").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -142,7 +142,7 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ .and("PERSON.LASTNAME", 7, undefined, SQLTYPES.INTEGER) .and("PERSON.LASTNAME", 8, "# <> ?", SQLTYPES.INTEGER) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and PERSON.LASTNAME <> ?").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and PERSON.LASTNAME <> ?").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(3).assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").elementAt(0).elementAt(1).equals(SQLTYPES.INTEGER).assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").elementAt(1).elementAt(1).equals(SQLTYPES.INTEGER).assert(); @@ -161,7 +161,8 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ "exists (select * FROM CONTACT where PERSON_ID = PERSONID)", [] ]) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.FIRSTNAME = ? ) and ( exists (select * FROM CONTACT where PERSON_ID = PERSONID) ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals(" ( PERSON.FIRSTNAME = ? ) and ( exists (select * FROM CONTACT where PERSON_ID = PERSONID) ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -176,7 +177,7 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ .where("PERSON.FIRSTNAME", "Tim") .and("PERSON.LASTNAME", "Admin")) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals(" ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -192,7 +193,8 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ .and("PERSON.LASTNAME", "Admin"), "exists ?") // Note: you can use SqlBuilder.EXISTS() instead of "exists ?" - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("exists ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals("exists ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -207,7 +209,8 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ .where("PERSON.FIRSTNAME", "Tim") .and("PERSON.LASTNAME", "Admin")) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals("PERSON.FIRSTNAME = ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -219,7 +222,8 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ .where(null, ["select FIRSTNAME from PERSON.FIRSTNAME = ?", [["Peter", 12]]], "exists ?") .and(null, ["exists (select FIRSTNAME from PERSON.FIRSTNAME = ?)", [["Peter", 12]]]) // also without pCond it should work as the condition could be included in the prep statement - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("exists ( select FIRSTNAME from PERSON.FIRSTNAME = ? ) and ( exists (select FIRSTNAME from PERSON.FIRSTNAME = ?) ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals("exists ( select FIRSTNAME from PERSON.FIRSTNAME = ? ) and ( exists (select FIRSTNAME from PERSON.FIRSTNAME = ?) ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -230,7 +234,7 @@ var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [ var actualValue = new SqlBuilder() .where("PERSON.FIRSTNAME", ["select FIRSTNAME from PERSON.FIRSTNAME = ?", [["Peter", 12]]]) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ( select FIRSTNAME from PERSON.FIRSTNAME = ? ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("PERSON.FIRSTNAME = ( select FIRSTNAME from PERSON.FIRSTNAME = ? ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -245,7 +249,7 @@ var validOrUsageTests = new TestSuite("SqlLib.validOrUsage", [ .where("PERSON.FIRSTNAME = 'Tim'") // NOTE: you should not do this as this does not add a real prepared statement with "?" .or("PERSON.LASTNAME = 'Admin'") - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = 'Tim' or PERSON.LASTNAME = 'Admin'").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("PERSON.FIRSTNAME = 'Tim' or PERSON.LASTNAME = 'Admin'").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0).assert(); } ), @@ -257,7 +261,7 @@ var validOrUsageTests = new TestSuite("SqlLib.validOrUsage", [ .where("PERSON.FIRSTNAME", "Tim") .or("PERSON.LASTNAME", "Admin") - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ?").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ?").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -269,7 +273,7 @@ var validOrUsageTests = new TestSuite("SqlLib.validOrUsage", [ .where("PERSON.FIRSTNAME", "Tim", "# <> ?") .or("PERSON.LASTNAME", "Admin") - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME <> ? or PERSON.LASTNAME = ?").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("PERSON.FIRSTNAME <> ? or PERSON.LASTNAME = ?").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -282,7 +286,7 @@ var validOrUsageTests = new TestSuite("SqlLib.validOrUsage", [ .or("PERSON.LASTNAME", 7, undefined, SQLTYPES.INTEGER) .or("PERSON.LASTNAME", 8, "# <> ?", SQLTYPES.INTEGER) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? or PERSON.LASTNAME <> ?").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? or PERSON.LASTNAME <> ?").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(3).assert(); } ), @@ -298,7 +302,8 @@ var validOrUsageTests = new TestSuite("SqlLib.validOrUsage", [ "exists (select * FROM CONTACT where PERSON_ID = PERSONID)", [] ]) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.FIRSTNAME = ? ) or ( exists (select * FROM CONTACT where PERSON_ID = PERSONID) ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals(" ( PERSON.FIRSTNAME = ? ) or ( exists (select * FROM CONTACT where PERSON_ID = PERSONID) ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -311,7 +316,7 @@ var validOrUsageTests = new TestSuite("SqlLib.validOrUsage", [ .where("PERSON.FIRSTNAME", "Tim") .or("PERSON.LASTNAME", "Admin")) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals(" ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -327,7 +332,8 @@ var validOrUsageTests = new TestSuite("SqlLib.validOrUsage", [ .or("PERSON.LASTNAME", "Admin"), "exists ?") - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("exists ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals("exists ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -342,7 +348,8 @@ var validOrUsageTests = new TestSuite("SqlLib.validOrUsage", [ .where("PERSON.FIRSTNAME", "Tim") .or("PERSON.LASTNAME", "Admin")) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals("PERSON.FIRSTNAME = ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -354,7 +361,8 @@ var validOrUsageTests = new TestSuite("SqlLib.validOrUsage", [ .where(null, ["select FIRSTNAME from PERSON.FIRSTNAME = ?", [["Peter", 12]]], "exists ?") .or(null, ["exists (select FIRSTNAME from PERSON.FIRSTNAME = ?)", [["Peter", 12]]]) // also without pCond it should work as the condition could be included in the prep statement - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("exists ( select FIRSTNAME from PERSON.FIRSTNAME = ? ) or ( exists (select FIRSTNAME from PERSON.FIRSTNAME = ?) ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals("exists ( select FIRSTNAME from PERSON.FIRSTNAME = ? ) or ( exists (select FIRSTNAME from PERSON.FIRSTNAME = ?) ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -371,7 +379,8 @@ var combinedAndOrTests = new TestSuite("SqlLib.combinedAndOr", [ .where("PERSON.FIRSTNAME", "Peter") .and("PERSON.LASTNAME", "Müller")) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("(PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?) or ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals("(PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?) or ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(4).assert(); } ), @@ -387,7 +396,8 @@ var combinedAndOrTests = new TestSuite("SqlLib.combinedAndOr", [ .where("PERSON.FIRSTNAME", "Peter") .or("PERSON.LASTNAME", "Müller")) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) and ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals(" ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) and ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(4).assert(); } ), @@ -411,7 +421,8 @@ var combinedAndOrTests = new TestSuite("SqlLib.combinedAndOr", [ .where("PERSON.FIRSTNAME", "Peter") .or("PERSON.LASTNAME", "Müller"))) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? or PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) or (PERSON.FIRSTNAME = ?) and PERSON.FIRSTNAME = ? or ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals("PERSON.FIRSTNAME = ? or PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) or (PERSON.FIRSTNAME = ?) and PERSON.FIRSTNAME = ? or ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(11).assert(); } ), @@ -426,7 +437,7 @@ var ifSetTests = new TestSuite("SqlLib.ifSet", [ .whereIfSet("PERSON.LASTNAME", null) .andIfSet("PERSON.LASTNAME", undefined) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("", "no sql should be added").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("", "no sql should be added").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0, "no params should be added").assert(); } ), @@ -439,7 +450,7 @@ var ifSetTests = new TestSuite("SqlLib.ifSet", [ var actualValue = new SqlBuilder() .whereIfSet("PERSON.FIRSTNAME", "$global.TestingVarNull") - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("", "no sql should be added").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("", "no sql should be added").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0, "no params should be added").assert(); } ), @@ -452,7 +463,7 @@ var ifSetTests = new TestSuite("SqlLib.ifSet", [ .andIfSet(["", []]) .andIfSet(new SqlBuilder()) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("", "no sql should be added").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("", "no sql should be added").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0, "no params should be added").assert(); } ), @@ -464,7 +475,7 @@ var ifSetTests = new TestSuite("SqlLib.ifSet", [ .whereIfSet("PERSON.FIRSTNAME", ["", []]) .andIfSet("PERSON.LASTNAME", new SqlBuilder()) - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("", "no sql should be added").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("", "no sql should be added").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0, "no params should be added").assert(); } ), @@ -885,7 +896,8 @@ var inStatementTests = new TestSuite("SqlLib.inStatement", [ .where("PERSON.LASTNAME", ["Franz", "Fritz"], SqlBuilder.IN()) // Note: you can use SqlBuilder.IN(), SqlBuilder.NOT_IN(), "# in ?", etc. as 3rd parameter .or("PERSON.LASTNAME", ["Peter", "Mayer"], SqlBuilder.IN()); - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.LASTNAME in (?, ?) ) or ( PERSON.LASTNAME in (?, ?) ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals(" ( PERSON.LASTNAME in (?, ?) ) or ( PERSON.LASTNAME in (?, ?) ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(4).assert(); } ), @@ -896,7 +908,7 @@ var inStatementTests = new TestSuite("SqlLib.inStatement", [ var actualValue = new SqlBuilder() .where("PERSON.LASTNAME", ["Franz", "Fritz"], "# not in ?"); // Note: you can use SqlBuilder.IN(), SqlBuilder.NOT_IN(), "# in ?", etc. as 3rd parameter - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.LASTNAME not in (?, ?) ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals(" ( PERSON.LASTNAME not in (?, ?) ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert(); } ), @@ -911,7 +923,8 @@ var inStatementTests = new TestSuite("SqlLib.inStatement", [ .where("PERSON.LASTNAME", "Fritz") , "# in ?"); // Note: you can use SqlBuilder.IN() instead of "# in ?" - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME in ( select PERSON.FIRSTNAME from PERSON where PERSON.LASTNAME = ? ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals("PERSON.FIRSTNAME in ( select PERSON.FIRSTNAME from PERSON where PERSON.LASTNAME = ? ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -923,7 +936,8 @@ var inStatementTests = new TestSuite("SqlLib.inStatement", [ .where("PERSON.FIRSTNAME", ["select PERSON.FIRSTNAME from PERSON where PERSON.LASTNAME = ?", [["Fritz", SQLTYPES.VARCHAR]]] , "# in ?"); // Note: you can use SqlBuilder.IN() instead of "# in ?" - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME in ( select PERSON.FIRSTNAME from PERSON where PERSON.LASTNAME = ? ) ").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals("PERSON.FIRSTNAME in ( select PERSON.FIRSTNAME from PERSON where PERSON.LASTNAME = ? ) ").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -934,7 +948,7 @@ var inStatementTests = new TestSuite("SqlLib.inStatement", [ var actualValue = new SqlBuilder() .whereIfSet("PERSON.LASTNAME", []); - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage").equals("").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0).assert(); } ), @@ -990,7 +1004,8 @@ var selectTests = new TestSuite("SqlLib.select", [ .select(["AB_ATTRIBUTEID", "AB_ATTRIBUTEUSAGEID", countSubQuery]) .from("AB_ATTRIBUTE") - pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select AB_ATTRIBUTEID, AB_ATTRIBUTEUSAGEID, (select count(*) from AB_ATTRIBUTEUSAGE where AB_ATTRIBUTEUSAGE.OBJECT_TYPE = ? and AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID)").assert(); + pTester.expectThat(actualValue).elementAt("_select").elementAt("sqlStorage") + .equals("select AB_ATTRIBUTEID, AB_ATTRIBUTEUSAGEID, (select count(*) from AB_ATTRIBUTEUSAGE where AB_ATTRIBUTEUSAGE.OBJECT_TYPE = ? and AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID)").assert(); pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -1006,7 +1021,7 @@ var selectTests = new TestSuite("SqlLib.select", [ .select("*") .from(subQuery) - pTester.expectThat(actualValue).elementAt("_from").elementAt("_sqlStorage").equals("from (select FIRSTNAME from PERSON where PERSON.LASTNAME = ?)").assert(); + pTester.expectThat(actualValue).elementAt("_from").elementAt("sqlStorage").equals("(select FIRSTNAME from PERSON where PERSON.LASTNAME = ?)").assert(); pTester.expectThat(actualValue).elementAt("_from").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -1026,7 +1041,7 @@ var joinTests = new TestSuite("SqlLib.join", [ .from("PERSON") .join("ORGANISATION", subQuery) - pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("_sqlStorage").equals("join ORGANISATION on ORGANISATION.NAME = ?").assert(); + pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("sqlStorage").equals("join ORGANISATION on ORGANISATION.NAME = ?").assert(); pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("preparedValues").hasLength(1).assert(); } ), @@ -1043,7 +1058,8 @@ var joinTests = new TestSuite("SqlLib.join", [ .from("PERSON") .join(subQuery, "orgname.NAME = TABLE2.NAME", "orgname") - pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("_sqlStorage").equals("join (select NAME from ORGANISATION where ORGANISATION.NAME = ?) orgname on orgname.NAME = TABLE2.NAME").assert(); + pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("sqlStorage") + .equals("join (select NAME from ORGANISATION where ORGANISATION.NAME = ?) orgname on orgname.NAME = TABLE2.NAME").assert(); pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("preparedValues").hasLength(1).assert(); } ), @@ -1056,7 +1072,7 @@ var joinTests = new TestSuite("SqlLib.join", [ .from("PERSON") .join("TABLE1 on TABLE1.NAME = TABLE2.NAME") - pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("_sqlStorage").equals("join TABLE1 on TABLE1.NAME = TABLE2.NAME").assert(); + pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("sqlStorage").equals("join TABLE1 on TABLE1.NAME = TABLE2.NAME").assert(); pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("preparedValues").hasLength(0).assert(); } ), @@ -1076,7 +1092,8 @@ var subqueryAsFieldTests = new TestSuite("SqlLib.subqueryAsField", [ .and("PERSON.FIRSTNAME", "val3"); - pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( ( select NAME from ORGANISATION where ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID and PERSON.FIRSTNAME = ? ) = ? ) and PERSON.FIRSTNAME = ?").assert(); + pTester.expectThat(actualValue).elementAt("_where").elementAt("sqlStorage") + .equals(" ( ( select NAME from ORGANISATION where ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID and PERSON.FIRSTNAME = ? ) = ? ) and PERSON.FIRSTNAME = ?").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(3).assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").elementAt(0).elementAt(0).equals("val1").assert(); pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").elementAt(1).elementAt(0).equals("val2").assert(); @@ -1185,7 +1202,8 @@ var subqueryAliasTests = new TestSuite("SqlLib.subqueryAlias", [ .select([subQuery, "FIRSTNAME"]) .from("PERSON") - pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select (select NAME from ORGANISATION where ORGANISATION.NAME = ?) testAlias, FIRSTNAME").assert(); + pTester.expectThat(actualValue).elementAt("_select").elementAt("sqlStorage") + .equals("select (select NAME from ORGANISATION where ORGANISATION.NAME = ?) testAlias, FIRSTNAME").assert(); pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -1201,7 +1219,7 @@ var subqueryAliasTests = new TestSuite("SqlLib.subqueryAlias", [ var actualValue = new SqlBuilder() .from(subQuery) - pTester.expectThat(actualValue).elementAt("_from").elementAt("_sqlStorage").equals("from (select NAME from ORGANISATION where ORGANISATION.NAME = ?) testAlias").assert(); + pTester.expectThat(actualValue).elementAt("_from").elementAt("sqlStorage").equals("(select NAME from ORGANISATION where ORGANISATION.NAME = ?) testAlias").assert(); pTester.expectThat(actualValue).elementAt("_from").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -1217,7 +1235,7 @@ var subqueryAliasTests = new TestSuite("SqlLib.subqueryAlias", [ var actualValue = new SqlBuilder() .from(subQuery, "overwriteAlias") - pTester.expectThat(actualValue).elementAt("_from").elementAt("_sqlStorage").equals("from (select NAME from ORGANISATION where ORGANISATION.NAME = ?) overwriteAlias").assert(); + pTester.expectThat(actualValue).elementAt("_from").elementAt("sqlStorage").equals("(select NAME from ORGANISATION where ORGANISATION.NAME = ?) overwriteAlias").assert(); pTester.expectThat(actualValue).elementAt("_from").elementAt("preparedValues").hasLength(1).assert(); } ), @@ -1235,15 +1253,49 @@ var subqueryAliasTests = new TestSuite("SqlLib.subqueryAlias", [ .join(subQuery, "testAlias.ORGANISATIONID = ORGANISATION_ID") .join(subQuery, "testAlias.ORGANISATIONID = ORGANISATION_ID", "overwriteAlias") - pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("_sqlStorage").equals("join (select NAME, ORGANISATIONID from ORGANISATION where ORGANISATION.NAME = ?) testAlias on testAlias.ORGANISATIONID = ORGANISATION_ID").assert(); - pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("preparedValues").hasLength(1).assert(); + pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("sqlStorage") + .equals("join (select NAME, ORGANISATIONID from ORGANISATION where ORGANISATION.NAME = ?) testAlias on testAlias.ORGANISATIONID = ORGANISATION_ID").assert(); + pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("preparedValues") + .hasLength(1).assert(); - pTester.expectThat(actualValue).elementAt("_joins").elementAt(1).elementAt("_sqlStorage").equals("join (select NAME, ORGANISATIONID from ORGANISATION where ORGANISATION.NAME = ?) overwriteAlias on testAlias.ORGANISATIONID = ORGANISATION_ID").assert(); + pTester.expectThat(actualValue).elementAt("_joins").elementAt(1).elementAt("sqlStorage") + .equals("join (select NAME, ORGANISATIONID from ORGANISATION where ORGANISATION.NAME = ?) overwriteAlias on testAlias.ORGANISATIONID = ORGANISATION_ID").assert(); pTester.expectThat(actualValue).elementAt("_joins").elementAt(1).elementAt("preparedValues").hasLength(1).assert(); } - ), + ) ]); +var setTableName = new TestSuite("SqlLib.setTableName", [ + new Test("tablename should be set by using .where", + function (pTester) + { + var sqlQuery = newWhere("ORGANISATION.NAME", "Adito"); + var actualValue = sqlQuery._tableName; + pTester.expectThat(actualValue).equals("ORGANISATION").assert(); + } + ) +]); + +var conditionIn = new TestSuite("SqlLib.conditionIn", [ + new Test("in condition shouldn't break with >1000 ids", + function (pTester) + { + var ids = new Array(1042); + for (let i = 0; i < ids.length; i++) + { + ids[i] = i.toString(); + } + var sqlQuery = newSelect("CONTACT.CONTACTID") + .from("CONTACT") + .where("CONTACT.CONTACTID", ids, SqlBuilder.IN()); + + pTester.expectThat(function () + { + sqlQuery.table(); + }).not().throwsException(); + } + ) +]); var tester = new Tester("Test SqlBuilder"); tester.test(newSelectTests); @@ -1260,6 +1312,8 @@ tester.test(joinTests); tester.test(subqueryAsFieldTests); tester.test(conditionFormatTests); tester.test(subqueryAliasTests); +tester.test(setTableName); +tester.test(conditionIn); tester.summary(); diff --git a/process/Sql_lib/process.js b/process/Sql_lib/process.js index 4ec5fbabe2..851b094fcf 100644 --- a/process/Sql_lib/process.js +++ b/process/Sql_lib/process.js @@ -9,1167 +9,1363 @@ import("system.SQLTYPES"); import("system.text"); import("Util_lib"); -/** - * object for easier handling of conditions; - * With this object you do not have to check if the string is empty or not; - * you don't need to append a "1=1" condition or similar; - * this objects gains most benefit if you have a lot of conditions that are added (or not) depending on tons of JDito-conditions +// see Documentation property of this lib for further explanation + +/** + * Creates a new SqlBuilder object and sets the select clause of the sql. * - * You can also use SqlCondition.begin(alias) for simpler object creation without new and without the need for an extra variable to save the object. + * @param {String|Array|SqlBuilder} pFields You can pass:<br/> + * - A String is just used AS IT IS. (e.g. "FIRSTNAME, LASTNAME")<br/> + * - SqlBuilder is used as Subquery<br/> + * - The array can also contain Strings, SqlBuilder which are just concatenated (e.g. ["FIRSTNAME", "LASTNAME", someSqlBuilderContainingFullSelect])<br/> + * Please see .select() for more information and examples. + * @param {String} [pAlias=currentAlias] This alias is used for fetching the ColumnTypes and also for the .table, .cell, .updateData, ... -functions + * @return {SqlBuilder} A new SqlBuilder object already containing the provided fields * - * @class - * @param {String} [alias=the current alias] the database alias where the condition shall be executed later (important for column types of preparedStatements) - * @example - * see others/guide/HowToSqlConditionLib.adoc + * @example + * var lastname = "Huber"; * - * @deprecated The SqlCondition will be removed in version >= 2020.x - * Use the SqlBuilder instead. - * For SqlBuilder usage see the documentation-property of the Sql_lib. + * var persons = newSelect("FIRSTNAME") + * .from("PERSON") + * .where("PERSON.LASTNAME", lastname) + * .arrayColumn(); */ -function SqlCondition(alias) { - // setting null is only needed to provide autocomplete for the ADITO-designer - this.preparedValues = null; - this._init(); // the properties are initalized in an extra function because init is nearly the same as resetting (clearing) the SqlConditions - this.alias = alias; - - // save, if the last condition was an OR. For better bracket-placement - this._lastWasOr = false; +function newSelect(pFields, pAlias) +{ + return new SqlBuilder(pAlias).select(pFields); } -/** - * Alternative possibility to crate a new condition. - * With this you don't need new SqlCondition and you can use the object directly after it's creation - * --> cleaner code +/** + * Creates a new SqlBuilder object and calls .where on it.<br/> + * This is very useful if you just need a condition as you can pass the first condition directly.<br/> + * Note: Even if you ommit all parameters, you do not have to call .where again. You can directly write .and / .or after newWhere().<br/> * - * It is very usefull for the orSqlCondition() and andSqlCondition() because now you can create a new condition inline. - * You can also use it for simple selects without the need to save the conditionObject in an extra variable. - * See Examples! + * @param {String|String[]|SqlBuilder|PreparedSqlArray} [pFieldOrCond] If this is the only parameter, it is used as Subselect <br/> + * else it is used as Field. <br/> + * Please see .where() for more information and examples. + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value whitch is used for the condition.<br/> + * Basically it can be nearly everything you need.<br/> + * Please see .where() for more information and examples. + * @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/> + * Please see .where() for more information and examples. + * @param {String} [pAlias=currentAlias] This alias is used for fetching the ColumnTypes and also for the .table, .cell, .updateData, ... -functions + * @return {SqlBuilder} A new SqlBuilder object which already called .where * - * @param {String} [alias=the current alias] the database alias where the condition shall be executed later (important for column types of preparedStatements) - * @return {SqlCondition} the new SqlCondition-object + * @example + * // examples, how where / whereIfSet could be used in a conditionProcess. + * //////Example 1///// + * var cond = newWhereIfSet("CONTACT.PERSON_ID", "$param.PersonId_param") + * .andIfSet("CONTACT.PERSON_ID", JSON.parse(vars.getString("$param.BlacklistPersons_param")), SqlBuilder.NOT_IN()) + * + * result.string(cond.toString()); * - * @example - * vars mySelect = SqlCondition.begin(alias) - * .and("MYID = '123'") - * .and(SqlCondition.begin() - * .or("NAME = 'Max'") - * .or("NAME = 'Bob'") - * ) - * .buildSql("select * from MYTABLE"); - * - * // Or use it for simple selects: - * var sum = db.cell(SqlCondition.begin() - * .andPrepared("STOCK.PRODUCT_ID", pid) - * .buildSql("select sum(QUANTITY * IN_OUT) from STOCK")); - * - * @deprecated The SqlCondition will be removed in version >= 2020.x - * Use the SqlBuilder instead. - * For SqlBuilder usage see the documentation-property of the Sql_lib." + * //////Example 2///// + * var cond = newWhere(); + * + * // note: we can use .and* now without an extra .where + * if (SOMECHECKS) + * cond.andIfSet(...) + * + * if (SOME_MORE_CHECKS) + * cond.and(...) + * + * result.string(cond.toString()); */ -SqlCondition.begin = function(alias) { - return new SqlCondition(alias); +function newWhere(pFieldOrCond, pValue, pCondition, pFieldType, pAlias) +{ + return new SqlBuilder(pAlias).where(pFieldOrCond, pValue, pCondition, pFieldType); } -/** - * checks if conditions have been added to the object - * @return {Boolean} true if conditions have been added, false when not +/** + * Creates a new SqlBuilder object and calls .whereIfSet on it.<br/> + * This is very useful if you just need a condition as you can pass the first condition directly.<br/> + * Note: Even if you ommit all parameters, you do not have to call .where again. You can directly write .and / .or after newWhere(). + * + * @param {String|String[]|SqlBuilder|PreparedSqlArray} [pFieldOrCond] If this is the only parameter, it is used as Subselect <br/> + * else it is used as Field. <br/> + * Please see .whereIfSet() for more information and examples. + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value whitch is used for the condition.<br/> + * Basically it can be nearly everything you need.<br/> + * Please see .whereIfSet() for more information and examples. + * @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/> + * Please see .whereIfSet() for more information and examples. + * @param {String} [pAlias=currentAlias] This alias is used for fetching the ColumnTypes and also for the .table, .cell, .updateData, ... -functions + * @return {SqlBuilder} A new SqlBuilder object which already called .whereIfSet + * + * @example + * // examples, how where / whereIfSet could be used in a conditionProcess. + * //////Example 1///// + * var cond = newWhereIfSet("CONTACT.PERSON_ID", "$param.PersonId_param") + * .andIfSet("CONTACT.PERSON_ID", JSON.parse(vars.getString("$param.BlacklistPersons_param")), SqlBuilder.NOT_IN()) + * + * result.string(cond.toString()); + * + * //////Example 2///// + * var cond = newWhere(); + * + * // note: we can use .and* now without an extra .where + * if (SOMECHECKS) + * cond.andIfSet(...) + * + * if (SOME_MORE_CHECKS) + * cond.and(...) + * + * result.string(cond.toString()); */ -SqlCondition.prototype.isSet = function() { - if (this._sqlStorage) - return true; - return false; +function newWhereIfSet(pFieldOrCond, pValue, pCondition, pFieldType, pAlias) +{ + return new SqlBuilder(pAlias).whereIfSet(pFieldOrCond, pValue, pCondition, pFieldType); } - /** - * append with SQL-and; no paranthesize of existing conditions is done - * @param {String} cond the condition string which shall be appended - * @return {SqlCondition} current SqlCondition-object + * Object for building sqls. The main purpose of this is to make it easy to use prepared statements.<br/> + * You should ALWAYS use prepared statemnts for Security and maybe also for performance reasons.<br/> + * If you are not used to use prepared statements all the time you WILL forget to use it, when it's really needed. (eg. direct user input, not just ids)<br/> + * <br/> + * This can also be useful to build complex sqls where parts should be added<br/> + * dynamically while keeping the code clean.<br/> + * <br/> + * There exist some shortcut funtions<br/> + * - if you need a full select use newSelect(...)<br/> + * - if you need only a condition use newWhere(...) or newWhereIfSet(...) + * + * @param {String} [pAlias=currentAlias] This alias is used for fetching the ColumnTypes and also for the .table, .cell, .updateData, ... -functions + * @class */ -SqlCondition.prototype.and = function(cond) { - if (!cond) - return this; - if (this.isSet()) - this._sqlStorage += " and "; - this._sqlStorage += cond; - return this; +function SqlBuilder (pAlias) +{ + if(!(this instanceof SqlBuilder)) + throw SqlBuilder._ERROR_INSTANCIATE_WITH_NEW(); + this._select = null; + this._from = null; + this._tableName = null; //for insert/update/delete + this._joins = []; + this._groupBy = null; + this._having = null; + this._orderBy = null; + this._unions = []; + this.alias = pAlias; + + //for paging + this._startRow = null; + this._pageSize = null; + this._hasMoreRows = true; + + this._subselectAlias = null; + + this._where = {}; + this._initWhere(); } /** - * append with SQL-or; Also paranthesize the existing conditions - * @param {String} cond the condition string which shall be appended - * @return {SqlCondition} current SqlCondition-object + * @return {Symbol} */ -SqlCondition.prototype.or = function(cond) { - if (!cond) - return this; - - if (this.isSet() && !this._lastWasOr) { - this._sqlStorage = "(" + this._sqlStorage + ") or (" + cond + ")"; - this._lastWasOr = true; - - } else if (this.isSet() && this._lastWasOr) { - this._sqlStorage = this._sqlStorage + " or (" + cond + ")"; - this._lastWasOr = true; - - } else { - this._sqlStorage = cond; - } - return this; +SqlBuilder.getCanBuildSqlSymbol = function () +{ + return Symbol["for"]("canBuildSql"); } -/** - * append a prepared-array to this sql condition with SQL-and - * @param {Array} preparedObj a prepared condition-array - * @return {SqlCondition} current SqlCondition-object - */ -SqlCondition.prototype.andAttachPrepared = function(preparedObj) { - if (preparedObj) - { - this.preparedValues = this.preparedValues.concat(preparedObj[1]); - return this.and(preparedObj[0]); - } - - return this; +SqlBuilder.defineCanBuildSql = function (pObject) +{ + pObject[SqlBuilder.getCanBuildSqlSymbol()] = true; +} + +SqlBuilder.checkCanBuildSql = function (pObject) +{ + return pObject[SqlBuilder.getCanBuildSqlSymbol()]; } +SqlBuilder.defineCanBuildSql(SqlBuilder.prototype); + /** - * append a prepared-array to this sql condition with SQL-or - * @param {Array} preparedObj a prepared condition-array - * @return {SqlCondition} current SqlCondition-object + * Deep copies the SqlBuilder object and returns a new one.<br/> + * Use this if you want to add for example add additional parameters without modifying the current builder. + * @return a full copy of the current SqlBuilder */ -SqlCondition.prototype.orAttachPrepared = function(preparedObj) { - if (preparedObj) +SqlBuilder.prototype.copy = function() +{ + return Utils.clone(this); +} + +// errors which are thrown by the SqlBuilder +SqlBuilder._ERROR_INSTANCIATE_WITH_NEW = function() +{ + return new Error(translate.text("SqlBuilder must be instanciated with 'new' or one of the factory methods (newSelect, newWhere, newWhereIfSet)")); +} + +SqlBuilder._ERROR_INVALID_CONDITION_VALUE_TYPE = function() +{ + return new Error(translate.text("SqlBuilder: invalid value-type for pCondition")); +} + +SqlBuilder._ERROR_NO_CONDITION = function() +{ + 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: 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: 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: 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: unsupportet parameter combination")); +} + +SqlBuilder._ERROR_NO_TABLE = function() +{ + return new Error(translate.text("SqlBuilder.deleteDat/updateData: You have to specify a tablename")); +} + +SqlBuilder._ERROR_NO_PARAMETER_PROVIDED = function() +{ + return new Error(translate.text("SqlBuilder: You have to specify at least one parameter")); +} + +SqlBuilder._ERROR_WHERE_NOT_FIRST = function() +{ + return new Error(translate.text("SqlBuilder: .where has to be called before following and/or.")); +} + +SqlBuilder._ERROR_ONLY_ONE_WHERE = function() +{ + return new Error(translate.text("SqlBuilder: .where has to be called only one time. Use and/or for further conditions.")); +} + +SqlBuilder._ERROR_INCOMPLETE_SELECT = function () +{ + return new Error(translate.text("SqlBuilder: select and from were expected, but not provided.")); +} + +SqlBuilder._ERROR_CONDITION_IS_MANDATORY = function () +{ + 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.")); +} + +SqlBuilder._ERROR_NOT_BOOLEAN = function () +{ + return new Error(translate.text("pExecuteOnlyIfConditionExists has to be of type boolean. This parameter controls what happens if the condition is empty (select / delete all or nothing)")); +} + +SqlBuilder._ERROR_UPDATE_VALUES_INVALID = function () +{ + return new Error(translate.text("SqlBuilder: The provided values object for updateFields is invalid or is not an object.")); +} + +SqlBuilder._ERROR_PAGESIZE_INVALID = function () +{ + return new Error(translate.text("SqlBuilder: The pagesize is not set or is not a number.")); +} + +SqlBuilder._ERROR_NOT_A_FUNCTION = function () +{ + return new Error(translate.text("SqlBuilder: The provided callback function is not a function.")); +} +/** + * Alternative way of creating a new SqlBuilder object that allows to use + * methods on it directly without having to put brackets around it + * + * @return {SqlBuilder} a new SqlBuilder object + * + * @example + * var query = SqlBuilder.begin() + * .select("ORGANISATION.NAME, FIRSTNAME, LASTNAME") + * .from("PERSON") + * .join("CONTACT", "CONTACT.PERSON_ID = PERSON.PERSONID") + * .leftJoin("ORGANISATION", SqlCondition.begin() + * .and("CONTACT.ORGANISATION_ID = ORGANISATION.ORGANISATIONID") + * .andPrepare("ORGANISATION.NAME", "S%", "# like ?") + * .build("1=2")) + * .where(SqlCondition.begin() + * .andPrepare("CONTACT.STATUS", $KeywordRegistry.contactStatus$active()) + * .build("1=2")); + * + * if (getCountry) //changing and adding parts + * { + * query.select("ORGANISATION.NAME, FIRSTNAME, LASTNAME, COUNTRY"); + * query.leftJoin("ADDRESS", "CONTACT.ADDRESS_ID = ADDRESS.ADDRESSID"); + * } + * + * var data = db.table(query.build()); + * + * @deprecated using .begin is deprecated as it's now possible to write "new SqlBuilder().select(...).from(...).... + You can now use "newSelect(...)", "newWhere(...)", "newWhereIfSet(...)" or "new SqlBuilder()" to create a new SqlBuilder instance. + For further SqlBuilder usage see the documentation-property of the Sql_lib. + */ +SqlBuilder.begin = function () +{ + return new SqlBuilder(); +} + + +/** + * Builds the sql and uses SqlUtils.translateXXXWithQuotes to make a string out of it. + * @param {String} [pDefaultConditionIfNone=""] the default condition string to use if the SqlBuilder contains no condition. In most cases you won't need this + * @param {Boolean} [pForceAsStatement=false] forces the use of SqlUtils.translateStatementWithQuotes even if it's no full statement. This is needed for example if you do not want brakets around the generated statement + * @return {String} the sql as string + */ +SqlBuilder.prototype.toString = function(pDefaultConditionIfNone, pForceAsStatement) +{ + var built = this.build(pDefaultConditionIfNone) + + if (built[0] !== "") { - this.preparedValues = this.preparedValues.concat(preparedObj[1]); - return this.or(preparedObj[0]); + if (!pForceAsStatement && !this.isFullSelect() && (this.hasCondition() || pDefaultConditionIfNone)) + return SqlUtils.translateConditionWithQuotes(built, this.alias); + else + return SqlUtils.translateStatementWithQuotes(built, this.alias); } - return this; + return ""; } /** - * append another condition with SQL-and + * Sets the select clause of the sql. + * @param {String|Array|SqlBuilder} pFields You can pass:<br/> + * - A String is just used AS IT IS. (e.g. "FIRSTNAME, LASTNAME")<br/> + * - SqlBuilder is used as Subquery<br/> + * - The array can also contain Strings, SqlBuilder which are just concatenated (e.g. ["FIRSTNAME", "LASTNAME", someSqlBuilderContainingFullSelect]) * - * @param {SqlCondition} cond the condition which shall be appended - * @param {String} [alternativeCond=""] condition if the given SqlCondition has none - * @return {SqlCondition} current SqlCondition-object + * @return {SqlBuilder} current SqlBuilder object */ -SqlCondition.prototype.andSqlCondition = function(cond, alternativeCond) { - if (!cond) - return this - - var otherCondition = cond.toString(alternativeCond); - if (otherCondition.trim() != "") - { - this.and(" ( " + cond.toString(alternativeCond) + " ) "); - if (cond.preparedValues) { - this.preparedValues = this.preparedValues.concat(cond.preparedValues); - } - } +SqlBuilder.prototype.select = function(pFields) +{ + this._select = SqlBuilder._getStatement(pFields, "select", undefined, true, true); return this; } /** - * append another condition with SQL-or; Also paranthesize the existing conditions - * - * @param {SqlCondition} cond the condition which shall be appended - * @param {String} [alternativeCond=""] condition if the given SqlCondition has none - * @return {SqlCondition} current SqlCondition-object + * Sets the select clause of the sql with distinct. + * @param {String|String[]} pFields + * @return {SqlBuilder} current SqlBuilder object */ -SqlCondition.prototype.orSqlCondition = function(cond, alternativeCond) { - var otherCondition = cond.toString(alternativeCond); - if (otherCondition.trim() != "") - { - this.or(" ( " + cond.toString(alternativeCond) + " ) "); - if (cond.preparedValues) { - this.preparedValues = this.preparedValues.concat(cond.preparedValues); - } - } +SqlBuilder.prototype.selectDistinct = function (pFields) +{ + this._select = SqlBuilder._getStatement(pFields, "select distinct", undefined, true, true); return this; } /** - * append an condition that uses a subQuery with SQL-and - * - * @param {SqlBuilder} subQuery the SqlBuilder object that will be used as a subquery - * @param {String} [cond="exists"] condition that is used (e. g. exists, not exists, COLUMN = any, COLUMN in, ...) - * @return {SqlCondition} current SqlCondition-object + * Sets the select clause to "select count(...)" + * @param {String} [pField=*] sql column to count, if omitted "count(*)" will be used + * @return {SqlBuilder} current SqlBuilder object */ -SqlCondition.prototype.andSqlBuilder = function(subQuery, cond) { - if (!cond) - cond = "exists"; - - var preparedObj = subQuery.build(); - preparedObj[0] = cond + " ( " + preparedObj[0] + " ) "; - this.andAttachPrepared(preparedObj); - +SqlBuilder.prototype.selectCount = function (pField) +{ + if (pField == undefined) + { + pField = "*"; + } + this._select = SqlBuilder._getStatement(pField, "select count(", ")", true, true); return this; } + /** - * append an condition that uses a subQuery with SQL-or + * sets an alias-name which is added at some places if this SqlBuilder is used as subselect (e.g. in .select(), .join(), .from(), ...) + * @param {String} pSubselectAlias * - * @param {SqlBuilder} subQuery the SqlBuilder object that will be used as a subquery - * @param {String} [cond="exists"] condition that is used (e. g. exists, not exists, COLUMN = any, COLUMN in, ...) - * @return {SqlCondition} current SqlCondition-object + * @return {SqlBuilder} current SqlBuilder object */ -SqlCondition.prototype.orSqlBuilder = function(subQuery, cond) { - if (!cond) - cond = "exists"; - - var preparedObj = subQuery.build(); - preparedObj[0] = cond + " ( " + preparedObj[0] + " ) "; - this.orAttachPrepared(preparedObj); - +SqlBuilder.prototype.subselectAlias = function(pSubselectAlias) +{ + this._subselectAlias = pSubselectAlias; return this; } /** - * same as the "and"-function but with preparedStatement functionality - * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] - * @param {String} value the value that shall be set into the prepared statement - * @param {String} [cond="# = ?"] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; - * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" - * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; - * The loaded type is cached if no type is given. So it is also safe to use this in a loop. - * e.g. - * for (...) { - * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") - * } - * @return {SqlCondition} current SqlCondition-object - */ -SqlCondition.prototype.andPrepare = function(field, value, cond, fieldType) { - cond = this._prepare(field, value, cond, fieldType); - return this.and(cond); -} - -/** - * same as the "or"-function but with preparedStatement functionality - * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] - * @param {String} value the value that shall be set into the prepared statement - * @param {String} [cond="# = ?"] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; - * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" - * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; - * The loaded type is cached if no type is given. So it is also safe to use this in a loop. - * e.g. - * for (...) { - * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") - * } - * @return {SqlCondition} current SqlCondition-object - */ -SqlCondition.prototype.orPrepare = function(field, value, cond, fieldType) { - cond = this._prepare(field, value, cond, fieldType); - return this.or(cond); -} - -/** - * same as the "andPrepare"-function but only applied if the passed "value" is truely - * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] - * @param {String} value the value that shall be set into the prepared statement - * @param {String} [cond="# = ?"] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; - * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" - * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; - * The loaded type is cached if no type is given. So it is also safe to use this in a loop. - * e.g. - * for (...) { - * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") - * } - * @return {SqlCondition} current SqlCondition-object + * Sets the table that is used for insert/update/delete functions. + * + * @param {String} pTable + * @return {SqlBuilder} current SqlBuilder object */ -SqlCondition.prototype.andPrepareIfSet = function(field, value, cond, fieldType) { - if (value) - return this.andPrepare(field, value, cond, fieldType); +SqlBuilder.prototype.tableName = function (pTable) +{ + this._tableName = pTable; return this; } /** - * same as the "orPrepare"-function but only applied if the passed "value" is truely - * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] - * @param {String} value the value that shall be set into the prepared statement - * @param {String} [cond="# = ?"] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; - * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" - * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; - * The loaded type is cached if no type is given. So it is also safe to use this in a loop. - * e.g. - * for (...) { - * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") - * } - * @return {SqlCondition} current SqlCondition-object + * Sets the from clause of the sql.<br/> + * <br/> + * Note: It is recommended to add joins via the .join functions.<br/> + * But in some cases you may already get a full from clause including the joins. In this case it is also possible to include them in the from-string.<br/> + * + * @param {String|SqlBuilder} pTable if it is a String, it is used as it is as table<br/> + * if it is a SqlBuilder, it is used as subselect: e.g. select * from (select FIRSTNAME from PERSON) + * @param {String} [pTableAlias] table alias + * @return {SqlBuilder} current SqlBuilder object */ -SqlCondition.prototype.orPrepareIfSet = function(field, value, cond, fieldType) { - if (value) - return this.orPrepare(field, value, cond, fieldType); +SqlBuilder.prototype.from = function(pTable, pTableAlias) +{ + this._from = SqlBuilder._getStatement(pTable, "", pTableAlias, false, (pTableAlias ? false : true)); + if (typeof(pTable) === "string") + this._tableName = pTable; return this; } /** - * same as the "andPrepare"-function but with validation of adito-variables functionality - * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] - * @param {String} variable the adito-variable that shall be set into the prepared statement - * @param {String} [cond = "# = ?" ] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; - * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" - * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; - * The loaded type is cached if no type is given. So it is also safe to use this in a loop. - * e.g. - * for (...) { - * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") - * } - * @return {SqlCondition} current SqlCondition-object + * Adds a join clause to the sql. + * @param {String|SqlBuilder} pTable if it is a String, it is used as it is as table<br/> + * if it is a SqlBuilder, it is used as subselect: e.g. select * from Table1 join (select FIRSTNAME from PERSON) on ... + * @param {String|SqlBuilder} [pCondition] The where condition. This can be<br/> + * - a string (without the where keyword)<br/> + * - a SqlBuilder NOTE: only the condition is used from it + * + * @param {String} [pTableAlias] This alias is used to add an alias to the tablename + * @param {String} [pPrefix] string before the join, for example "left", "right" + * @param {String} [pReplacementForWordJoin] if this is set, this is used instead of the word "join". Needed for e.g. OUTER APPLY in MSSQL + * @return {SqlBuilder} current SqlBuilder object */ -SqlCondition.prototype.andPrepareVars = function(field, variable, cond, fieldType) { - variable = this._checkVars(variable) - if (variable) { - return this.andPrepare(field, variable, cond, fieldType); +SqlBuilder.prototype.join = function(pTable, pCondition, pTableAlias, pPrefix, pReplacementForWordJoin) +{ + // support for deprecated SqlCondition + if (pCondition instanceof SqlCondition) + { + pCondition = newWhere(pCondition); + logging.log("Warning: using .where with a SqlCondition as pFieldOrCond is deprecated. The SqlCondition will be removed in version >= 2020.x\n" + + "For SqlBuilder usage see the documentation-property of the Sql_lib."); } - return this; -} + + var prefix = (pReplacementForWordJoin ? pReplacementForWordJoin : "join"); + if (pPrefix) + prefix = pPrefix + " " + prefix; + + var postfix = pCondition ? "on" : ""; + + if (pTableAlias) + postfix = pTableAlias + " " + postfix; + else if (pTable instanceof SqlBuilder && pTable._subselectAlias) + postfix = pTable._subselectAlias + " " + postfix; + + var joinPart = SqlBuilder._getStatement(pTable, prefix, postfix.trim()); + + if (pCondition) + { + if (pCondition instanceof SqlBuilder) + pCondition = [pCondition._where.sqlStorage, pCondition._where.preparedValues] -/** - * same as the "orPrepare"-function but with validation of adito-variables functionality - * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] - * @param {String} variable the adito-variable that shall be set into the prepared statement - * @param {String} [cond="# = ?"] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; - * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" - * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; - * The loaded type is cached if no type is given. So it is also safe to use this in a loop. - * e.g. - * for (...) { - * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") - * } - * @return {SqlCondition} current SqlCondition-object - */ -SqlCondition.prototype.orPrepareVars = function(field, variable, cond, fieldType) { - variable = this._checkVars(variable) - if (variable) { - return this.orPrepare(field, variable, cond, fieldType); + var conditionPart = SqlBuilder._getStatement(pCondition); + + joinPart.sqlStorage += " " + conditionPart.sqlStorage; + joinPart.preparedValues = joinPart.preparedValues.concat(conditionPart.preparedValues); } + + this._joins.push(joinPart) return this; } /** - * creates a IN-statement out of a field and an array of values. - * Be carefull with a big number of values. This may have a bad performance. + * Adds a left join clause to the sql. * - * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] - * @param {String[]} values the value that shall be set into the prepared statement - * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; - * The loaded type is cached if no type is given. So it is also safe to use this in a loop. - * e.g. - * for (...) { - * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") - * } - * @param {Boolean} [not = undefined] if true, add not before in - * @return {SqlCondition} current SqlCondition-object + * @param {String|SqlBuilder} pTable if it is a String, it is used as it is as table<br/> + * if it is a SqlBuilder, it is used as subselect: e.g. select * from Table1 join (select FIRSTNAME from PERSON) on ... + * @param {String|SqlBuilder} [pCondition] The where condition. This can be<br/> + * - a string (without the where keyword)<br/> + * - a SqlBuilder NOTE: only the condition is used from it + * + * @param {String} [pTableAlias] This alias is used to add an alias to the tablename + * @return {SqlBuilder} current SqlBuilder object */ -SqlCondition.prototype.andIn = function(field, values, fieldType, not) { - return this.andAttachPrepared(this._in(field, values, fieldType, not)); +SqlBuilder.prototype.leftJoin = function(pTable, pCondition, pTableAlias) +{ + return this.join(pTable, pCondition, pTableAlias, "left"); } /** - * creates a IN-statement out of a field and an array of values. - * Be carefull with a big number of values. This may have a bad performance. + * Adds a right join clause to the sql. * - * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] - * @param {String[]} values the value that shall be set into the prepared statement - * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; - * The loaded type is cached if no type is given. So it is also safe to use this in a loop. - * e.g. - * for (...) { - * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") - * } - * @param {Boolean} [not = undefined] if true, add not before in - * @return {SqlCondition} current SqlCondition-object + * @param {String|SqlBuilder} pTable if it is a String, it is used as it is as table<br/> + * if it is a SqlBuilder, it is used as subselect: e.g. select * from Table1 join (select FIRSTNAME from PERSON) on ... + * @param {String|SqlBuilder} [pCondition] The where condition. This can be<br/> + * - a string (without the where keyword)<br/> + * - a SqlBuilder NOTE: only the condition is used from it + * + * @param {String} [pTableAlias] This alias is used to add an alias to the tablename + * @return {SqlBuilder} current SqlBuilder object */ -SqlCondition.prototype.orIn = function(field, values, fieldType, not) { - return this.orAttachPrepared(this._in(field, values, fieldType, not)); +SqlBuilder.prototype.rightJoin = function(pTable, pCondition, pTableAlias) +{ + return this.join(pTable, pCondition, pTableAlias, "right"); } /** - * creates a IN-statement out of a field and an array of values. - * Be carefull with a big number of values. This may have a bad performance. + * Throws an error if pValue is null, undefined or a SqlBuilder without condition (or if pValue is a $-variable: error if the result of it is null or undefined)<br/> + * Also throws an error if pFieldOrCond is the only parameter and it is null<br/> + * <br/> + * Starts the where clause of the SQL. You may pass the first condition with it.<br/> + * But you can also call this function without any parameter and add the conditions with subsequent .and / .or<br/> + * <br/> + * This method exists mainly for semantic reasons and can only be callled once.<br/> + * As shourtcut you could use the newWhere(...) function.<br/> * - * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] - * @param {String[]} values the value that shall be set into the prepared statement - * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; - * The loaded type is cached if no type is given. So it is also safe to use this in a loop. - * e.g. - * for (...) { - * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") - * } - * @param {Boolean} [not = undefined] if true, add not before in - * @return {SqlCondition} current SqlCondition-object - */ -SqlCondition.prototype._in = function(field, values, fieldType, not) { - if (values && values.length > 0) - { - if (fieldType == undefined) - fieldType = SqlUtils.getSingleColumnType(field, undefined, this.alias); - - preparedStatement = SqlUtils.getSqlInStatement(field, values, undefined, true, fieldType); - if (not) - preparedStatement[0] = " not " + preparedStatement[0]; - return preparedStatement; - } - - return null; -} - -/** - * ready to use string; does not contain a where keyword at the beginning - * @param {String} [alternativeCond=""] condition that is returned when nothing has been appended. - * @return {String} concatenated SQL-condition; empty string if nothing has been appended or - if passed - the alternativeCond - */ -SqlCondition.prototype.toString = function(alternativeCond) { - if (!this.isSet() && alternativeCond) - return alternativeCond - else - return this._sqlStorage; -} - -/** - * ready to use string; does contain a where keyword at the beginning - * @param {String} [alternativeCond=""] condition that is returned when nothing has been appended. - * @return {SqlCondition} concatenated SQL-condition; empty string if nothing has been appended or - if passed - the alternativeCond - */ -SqlCondition.prototype.toWhereString = function(alternativeCond) { - var cond = this.toString(alternativeCond); - if (cond) - return " where " + cond; - else - return cond; -} - -/** - * ready to use prepared condition; does not contain a where keyword at the beginning - * @param {String} [alternativeCond=""] Condition that is returned when nothing has been appended. - * @return {Array[][][]} Prepared condition with [condition, [[field1, type1], [field2, type2]]] - */ -SqlCondition.prototype.build = function(alternativeCond) { - return [this.toString(alternativeCond), this.preparedValues]; -} - -/** - * ready to use prepared select - * @param {String} pBeforeCondition Part of the sql before the condition without where (e.g. "select FIRSTNAME from PERSON") - * @param {String} [pAlternativeCond=""] Condition that is returned when nothing has been appended. - * @param {String} [pAfterCondition=""] Part of the sql after the condition (e.g. "order by FIRSTNAME"). - * @param {Boolean} [pWithWere=true] true if where should be added to the bginning - * @return {Array[][][]} Prepared condition with [condition, [[field1, type1], [field2, type2]]] + * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] If this is the only parameter, it is used as Condition <br/> + * else it is used as Field.<br/> + * <br/> + * If you use it as Subselect (the only param), it can be:<br/> + * - a string: just added as it is<br/> + * - a PreparedSqlArray: an Array in this form: [sqlStr, [[value1, type1], [valueN, typeN]]]<br/> + * the sql is just used as it is.<br/> + * - a SqlBuilder: ONLY THE CONDITION is used from it<br/> + * <br/> + * If you use it as a Field (at least pValue has to be filled), this param provides the field information to<br/> + * load the SQLTYPE for this condition. <br/> + * It can be provided in the following ways:<br/> + * - a string: ONLY in this form: "TABLENAME.COLUMNNAME" <br/> + * Note1: you may have problems with names containing a '.' Use the next variant (as array) in this case<br/> + * Note2: if you need a table alias use the next variant (as array)<br/> + * - a array: ["TABLENAME", "COLUMNNAME", "tableAlias"] OR ["TABLENAME", "COLUMNNAME"]<br/> + * - a SqlBuilder: the full select is used as subselect and compared with pValue. <br/> + * (e.g. select * from PERSON where (select "NAME" from ORGANISATION where ... ) = ?)<br/> + * Note: for this you have to provide pFieldType as the type cannot be calculated from the subselect!<br/> + * Note: this can also be null if you don't need the field and use a pCondition without a # + * + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value which is used for the condition.<br/> + * Basically it can be nearly everything you need.<br/> + * - String, etc: is just used as value for the prepared statement. Of course it has to fit the type of the db-column<br/> + * - String starting with '$' is treated as jdito-variable: is loaded with vars.getString("$..."). <br/> + * Note: Use 2 '$' to escape the $ if you don't want it to be treated as JditoVar + * + * @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/> + * This is helpful if you for example have a pCondition "year(#) = ?"<br/> + * then the db-field is DATETIME, but the value is INTEGER. In this case you can overwrite the type. + * + * @return {SqlBuilder} current SqlBuilder object */ -SqlCondition.prototype.buildSql = function(pBeforeCondition, pAlternativeCond, pAfterCondition, pWithWere) { - if (pAfterCondition == undefined) - pAfterCondition = ""; - - if (pWithWere == undefined) - pWithWere = true; +SqlBuilder.prototype.where = function(pFieldOrCond, pValue, pCondition, pFieldType) +{ + // support for deprecated SqlCondition + if (pFieldOrCond instanceof SqlCondition && pValue === undefined && pCondition === undefined && pFieldType === undefined) + { + let copiedCondition = newWhere(); + + copiedCondition._where.preparedValues = pFieldOrCond.preparedValues; + copiedCondition._where._lastWasOr = pFieldOrCond._lastWasOr; + copiedCondition._where.sqlStorage = pFieldOrCond.sqlStorage; + + pFieldOrCond = copiedCondition; + + logging.log("Warning: using .where with a SqlCondition as pFieldOrCond is deprecated. The SqlCondition will be removed in version >= 2020.x\n" + + "For SqlBuilder usage see the documentation-property of the Sql_lib."); + } - return [pBeforeCondition + " " + - (pWithWere ? this.toWhereString(pAlternativeCond) : this.toString(pAlternativeCond)) + - " " + pAfterCondition, this.preparedValues]; + return this._setWhere(pFieldOrCond, pValue, pCondition, pFieldType, this.or); } /** - * translates SqlCondition to plain SQL. Use this if prepared statements are not supported. - * It resolves all prepared values. - * @param {String} pAlternativeCond used if the SqlCondition does not contain any condition. - * @return {String} plain SQL condition + * Difference to where(): where throws errors on invalid values, whereIfSet just ignores the condition and does nothing (usefull e.g. for the parameter variables ("$param.ddd") in conditionProcesses.)<br/> + * <br/> + * Starts the whereIfSet clause of the SQL. You may pass the first condition with it.<br/> + * But you can also call this function without any parameter and add the conditions with subsequent .and / .or<br/> + * <br/> + * This method exists mainly for semantic reasons and can only be callled once.<br/> + * As shourtcut you could use the newWhereIfSet(...) function. + * + * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] If this is the only parameter, it is used as Condition <br/> + * else it is used as Field.<br/> + * <br/> + * If you use it as Subselect (the only param), it can be:<br/> + * - a string: just added as it is<br/> + * - a PreparedSqlArray: an Array in this form: [sqlStr, [[value1, type1], [valueN, typeN]]]<br/> + * the sql is just used as it is.<br/> + * - a SqlBuilder: ONLY THE CONDITION is used from it<br/> + * <br/> + * If you use it as a Field (at least pValue has to be filled), this param provides the field information to<br/> + * load the SQLTYPE for this condition. <br/> + * It can be provided in the following ways:<br/> + * - a string: ONLY in this form: "TABLENAME.COLUMNNAME" <br/> + * Note1: you may have problems with names containing a '.' Use the next variant (as array) in this case<br/> + * Note2: if you need a table alias use the next variant (as array)<br/> + * - a array: ["TABLENAME", "COLUMNNAME", "tableAlias"] OR ["TABLENAME", "COLUMNNAME"]<br/> + * - a SqlBuilder: the full select is used as subselect and compared with pValue. <br/> + * (e.g. select * from PERSON where (select "NAME" from ORGANISATION where ... ) = ?)<br/> + * Note: for this you have to provide pFieldType as the type cannot be calculated from the subselect!<br/> + * Note: this can also be null if you don't need the field and use a pCondition without a # + * + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value which is used for the condition.<br/> + * Basically it can be nearly everything you need.<br/> + * - String, etc: is just used as value for the prepared statement. Of course it has to fit the type of the db-column<br/> + * - String starting with '$' is treated as jdito-variable: is loaded with vars.getString("$..."). <br/> + * Note: Use 2 '$' to escape the $ if you don't want it to be treated as JditoVar + * + * @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/> + * This is helpful if you for example have a pCondition "year(#) = ?"<br/> + * then the db-field is DATETIME, but the value is INTEGER. In this case you can overwrite the type. + * + * @return {SqlBuilder} current SqlBuilder object */ -SqlCondition.prototype.translate = function(pAlternativeCond) +SqlBuilder.prototype.whereIfSet = function(pFieldOrCond, pValue, pCondition, pFieldType) { - return SqlUtils.translateConditionWithQuotes(this.build(pAlternativeCond, this.alias)); + return this._setWhere(pFieldOrCond, pValue, pCondition, pFieldType, this.orIfSet); } /** - * Check if (adito-)variable exists and vars.getString is not empty - * @param {String} variable the variable name (e.g. "$field.CONTACT_ID") - * @return {String | Boolean} The value of the field as string OR false if it doesn't exist. + * helper function for .where and .whereIfSet because they do almost the same<br/> + * See .where() for further explanations + * + * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] + * @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() * + * @return {SqlBuilder} current SqlBuilder object * @ignore */ -SqlCondition.prototype._checkVars = function(variable) { - if (vars.exists(variable)) { - var value = vars.getString(variable); - if (value) { - return value; - } +SqlBuilder.prototype._setWhere = function (pFieldOrCond, pValue, pCondition, pFieldType, pAddCondFn) +{ + // allow where-call without parameter to just enable where mode + if (pFieldOrCond === undefined && pValue === undefined && pCondition === undefined && pFieldType === undefined) + { + this._where._whereWasCalled = true; + return this; } - return false; + + // where has to be called before all other and / or + if (this.hasCondition()) + throw SqlBuilder._ERROR_WHERE_NOT_FIRST(); + + // only one where call is allowed + if (this._where._whereWasCalled) + throw SqlBuilder._ERROR_ONLY_ONE_WHERE(); + + this._where._whereWasCalled = true; + return pAddCondFn.call(this, pFieldOrCond, pValue, pCondition, pFieldType); } /** - * hidden function for composing preparedStatements - * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] - * @param {String} value the value that shall be set into the prepared statement - * @param {String} cond the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; - * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" - * Default is "# = ?" - * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; - * The loaded type is cached if no type is given. So it is also safe to use this in a loop. - * e.g. - * for (...) { - * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") - * } - * @return {String} the replaced SQL-condition string (replace # by the fieldname) + * helper function which adds a condition + * + * @param {String|SqlBuilder|PreparedSqlArray} pCondition the condition to add + * @param {Boolean} [pMandatory=true] if true: throws error on SqlBuilder without conditon or PreparedSqlArray with empty string. Else: just does nothing + * @param {CallbackFunction} pAddPreparedConditionCallback A Callback funtion which receives a PreparedSqlArray as parameter + * @param {Boolean} pBrackets if true, Brackets are added in some cases + * + * @return {SqlBuilder} current SqlBuilder object * @ignore */ -SqlCondition.prototype._prepare = function(field, value, cond, fieldType) { - if (value == undefined) - { - throw new Error(translate.withArguments("${SQL_LIB_UNDEFINED_VALUE} field: %0", [field])); - } +SqlBuilder.prototype._addWhereCondition = function(pCondition, pMandatory, pAddPreparedConditionCallback, pBrackets) +{ + if (pCondition === undefined) + return this; - if (cond == undefined) { - cond = "# = ?" - } - - var alias; + if (pMandatory === undefined) + pMandatory = true; - if (typeof field === 'string') + var sql = pCondition; + + // the field is a simple string -> just add the string, no prepared statement + if (Utils.isString(sql)) { - var pointPos = field.indexOf("."); - - if (pointPos > 0 && pointPos < field.length-1) - { - alias = field; - } - else - { - throw new Error(translate.text("${SQL_LIB_FIELD_WRONG_FORMAT}") + field + translate.withArguments("${SQL_LIB_FIELD_WRONG_FORMAT} field: %0", [field])); - } + pAddPreparedConditionCallback.call(this, [sql, []]); + return this; } - else + + // the field is an array -> it is a prepared condition + if (Array.isArray(sql)) { - if (field.length == 3) - { - alias = field[2] + "." + field[1]; - field = field[0] + "." + field[1]; - } - else + if (sql[0]) { - throw new Error(translate.text("${SQL_LIB_FIELD_WRONG_FORMAT}") + field.toSource() + translate.withArguments("${SQL_LIB_FIELD_WRONG_FORMAT} field: %0", [field.toSource()])); - } - } - - var type; - - if (fieldType == undefined) - fieldType = SqlUtils.getSingleColumnType(field, undefined, this.alias); + this._where.preparedValues = this._where.preparedValues.concat(sql[1]); - //this function looks more complex (and slower) than it actually is - /* the following regex looks like this after javascript-escaping of the backslash: (?<!\\)((?:\\\\)*)# - the regexp searches for the unescaped character and these characters are replaced by the field name + // add only brackets if needed + if (pBrackets) + sql[0] = " ( " + sql[0] + " ) "; - examples: - --------------------- - | # --match | - | \# --no-match | - | \\# --match | - | \\\# --no-match | - | \\\\# --match | - --------------------- - */ - //use replaceAll because it's faster and supports negative lookbehinds - cond = text.replaceAll(cond, { - //manually readd the replaced backslashes by using a group reference, because they a part of the match and therefore replaced by "replaceAll" - //since the field COULD contain already a group reference (I think this is extremely uncommon; - //probably that never happens but better stay save): escape that references within the fieldname - "(?<!\\\\)((?:\\\\\\\\)*)#": "$1" + text.replaceAll(alias, { - "$1": "\\$1" - }), - //now that we've replaced the correct field placeholder let's replace the escaped number sign "\#" to a normal number sign "#" - "\\\\#": "#" - }); - - - - type = fieldType - this.preparedValues.push([value.toString(), type]); - return cond; -} + pAddPreparedConditionCallback.call(this, [sql[0], []], pBrackets) + return this; + } + else if (pMandatory) + throw SqlBuilder._ERROR_CONDITION_IS_MANDATORY(); + + return this; + } + // the field is a SqlBuilder -> it is a SqlBuilder which contains a condition -> the condition of the SqlBuilder is added. + if (sql instanceof SqlBuilder) + { + // add only brackets if needed + var sqlString = sql._where.sqlStorage; + + + var condString = sqlString; + if (condString.trim() != "") + { + if (pBrackets) + condString = " ( " + condString + " ) "; + + pAddPreparedConditionCallback.call(this, [condString, sql._where.preparedValues], pBrackets); + return this; + } + else if (pMandatory) + throw SqlBuilder._ERROR_CONDITION_IS_MANDATORY(); + + return this; + } -/** - * function that resets the current SqlCondition as if no conditions would have been added - * this is usefull if you want to reuse the same object over and over - * @return {null} - */ -SqlCondition.prototype.clear = function() { - this._sqlStorage = ""; - this.preparedValues = []; - return this; + throw SqlBuilder._ERROR_INVALID_CONDITION_VALUE_TYPE(); } /** - * hidden function for initializing all properties for the sql conditions - * @return {null} + * helper function which adds a Subquery-condition * + * @param {SqlBuilder|PreparedSqlArray} pSubquery the subquery to add + * @param {Boolean} [pMandatory=true] if true: throws error on SqlBuilder without conditon or PreparedSqlArray with empty string. Else: just does nothing + * @param {Boolean} pCondition the condition to be used: e.g. "exists(?)" the ? is replaced by the subquery + * @param {CallbackFunction} pAddPreparedConditionCallback A Callback funtion which receives a PreparedSqlArray as parameter + * + * @return {SqlBuilder} current SqlBuilder object * @ignore */ -SqlCondition.prototype._init = function() { - //init only wraps the clear function to avoid confusion in the constructor (and provide better extensibility) - return this.clear(); -} - -// some static functions for often used tasks. They are only provided for very simple tasks. +SqlBuilder.prototype._addWhereSubquery = function(pSubquery, pMandatory, pCondition, pAddPreparedConditionCallback) +{ + if (pSubquery === undefined) + return this; -/** - * pField = pValue - * @param {String} pField the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" - * @param {String} pValue the value that shall be set into the prepared statement - * @param {String} [pAlternativeCond=""] Condition that is returned when nothing has been appended. - * @param {String} [pAlias=the current alias] the database alias where the condition shall be executed later (important for column types of preparedStatements) - * - * @return {Array[][][]} Prepared condition with [condition, [[field, type]]] - * - * @deprecated - */ -SqlCondition.equals = function(pField, pValue, pAlternativeCond, pAlias) { - return SqlCondition["begin"](pAlias).andPrepare(pField, pValue).build(pAlternativeCond); -} + if (pMandatory === undefined) + pMandatory = true; + + var sql = pSubquery; -/** - * pField <> pValue - * @param {String} pField the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" - * @param {String} pValue the value that shall be set into the prepared statement - * @param {String} [pAlternativeCond=""] Condition that is returned when nothing has been appended. - * @param {String} [pAlias=the current alias] the database alias where the condition shall be executed later (important for column types of preparedStatements) - * - * @return {Array[][][]} Prepared condition with [condition, [[field, type]]] - * - * @deprecated - */ -SqlCondition.equalsNot = function(pField, pValue, pAlternativeCond, pAlias) { - return SqlCondition["begin"](pAlias).andPrepare(pField, pValue, "# <> ?").build(pAlternativeCond); -} + // the field is an array -> it is a prepared statement which already SHOULD contain exists or another condition + // Both can be handled by _prepare + if (Array.isArray(sql)) + { + if (sql[0]) + pAddPreparedConditionCallback.call(this, this._prepare(undefined, sql, pCondition)); + else if (pMandatory) + throw SqlBuilder._ERROR_VALUE_IS_MANDATORY(); + + return this; + } -// see Documentation property of this lib for further explanation + // the field is a SqlBuilder -> it is a SqlBuilder which contains a condition -> the condition of the SqlBuilder is added. + if (sql instanceof SqlBuilder) + { + var subQuery = pSubquery; -/** - * Creates a new SqlBuilder object and sets the select clause of the sql. - * - * @param {String|Array|SqlBuilder} pFields You can pass:<br/> - * - A String is just used AS IT IS. (e.g. "FIRSTNAME, LASTNAME")<br/> - * - SqlBuilder is used as Subquery<br/> - * - The array can also contain Strings, SqlBuilder which are just concatenated (e.g. ["FIRSTNAME", "LASTNAME", someSqlBuilderContainingFullSelect])<br/> - * Please see .select() for more information and examples. - * @param {String} [pAlias=currentAlias] This alias is used for fetching the ColumnTypes and also for the .table, .cell, .updateData, ... -functions - * @return {SqlBuilder} A new SqlBuilder object already containing the provided fields - * - * @example - * var lastname = "Huber"; - * - * var persons = newSelect("FIRSTNAME") - * .from("PERSON") - * .where("PERSON.LASTNAME", lastname) - * .arrayColumn(); - */ -function newSelect(pFields, pAlias) -{ - return new SqlBuilder(pAlias).select(pFields); + // Without condition this function cannot be used with SqlBuilder object as it cannot contain a condition + if (!pCondition) + throw SqlBuilder._ERROR_NO_CONDITION(); + + if (subQuery.isFullSelect() || subQuery.hasCondition()) //can also be only an condition if SqlBuilder.NOT() is used + { + var preparedObj = subQuery.build(); + pAddPreparedConditionCallback.call(this, this._prepare(undefined, preparedObj, pCondition)); + } + else if (pMandatory) + throw SqlBuilder._ERROR_VALUE_IS_MANDATORY(); + + return this; + } + + throw SqlBuilder._ERROR_INVALID_SUBQUERY_TYPE(); } -/** - * Creates a new SqlBuilder object and calls .where on it.<br/> - * This is very useful if you just need a condition as you can pass the first condition directly.<br/> - * Note: Even if you ommit all parameters, you do not have to call .where again. You can directly write .and / .or after newWhere().<br/> - * - * @param {String|String[]|SqlBuilder|PreparedSqlArray} [pFieldOrCond] If this is the only parameter, it is used as Subselect <br/> - * else it is used as Field. <br/> - * Please see .where() for more information and examples. - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value whitch is used for the condition.<br/> - * Basically it can be nearly everything you need.<br/> - * Please see .where() for more information and examples. - * @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/> - * Please see .where() for more information and examples. - * @param {String} [pAlias=currentAlias] This alias is used for fetching the ColumnTypes and also for the .table, .cell, .updateData, ... -functions - * @return {SqlBuilder} A new SqlBuilder object which already called .where - * - * @example - * // examples, how where / whereIfSet could be used in a conditionProcess. - * //////Example 1///// - * var cond = newWhereIfSet("CONTACT.PERSON_ID", "$param.PersonId_param") - * .andIfSet("CONTACT.PERSON_ID", JSON.parse(vars.getString("$param.BlacklistPersons_param")), SqlBuilder.NOT_IN()) - * - * result.string(cond.toString()); +/** + * helper function which adds a condition to the where * - * //////Example 2///// - * var cond = newWhere(); + * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} pFieldOrCond see .where() + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} pValue see .where() + * @param {Boolean} pMandatory if true: throw error if pValue is null, undefined, SqlBuilder without condition, etc... else just ignore the condition + * @param {String} [pCondition="# = ?"] see .where() + * @param {SQLTYPES|Numeric} [pFieldType=AutomaticallyLoadedType] see .where() + * @param {CallbackFunction} pAddPreparedConditionCallback A Callback funtion which receives a PreparedSqlArray as parameter * - * // note: we can use .and* now without an extra .where - * if (SOMECHECKS) - * cond.andIfSet(...) - * - * if (SOME_MORE_CHECKS) - * cond.and(...) - * - * result.string(cond.toString()); + * @return {SqlBuilder} current SqlBuilder object + * @ignore */ -function newWhere(pFieldOrCond, pValue, pCondition, pFieldType, pAlias) +SqlBuilder.prototype._addWhere = function(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType, pAddPreparedConditionCallback) { - return new SqlBuilder(pAlias).where(pFieldOrCond, pValue, pCondition, pFieldType); -} + pCondition = this._verifyConditionFormat(pCondition); -/** - * Creates a new SqlBuilder object and calls .whereIfSet on it.<br/> - * This is very useful if you just need a condition as you can pass the first condition directly.<br/> - * Note: Even if you ommit all parameters, you do not have to call .where again. You can directly write .and / .or after newWhere(). - * - * @param {String|String[]|SqlBuilder|PreparedSqlArray} [pFieldOrCond] If this is the only parameter, it is used as Subselect <br/> - * else it is used as Field. <br/> - * Please see .whereIfSet() for more information and examples. - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value whitch is used for the condition.<br/> - * Basically it can be nearly everything you need.<br/> - * Please see .whereIfSet() for more information and examples. - * @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/> - * Please see .whereIfSet() for more information and examples. - * @param {String} [pAlias=currentAlias] This alias is used for fetching the ColumnTypes and also for the .table, .cell, .updateData, ... -functions - * @return {SqlBuilder} A new SqlBuilder object which already called .whereIfSet - * - * @example - * // examples, how where / whereIfSet could be used in a conditionProcess. - * //////Example 1///// - * var cond = newWhereIfSet("CONTACT.PERSON_ID", "$param.PersonId_param") - * .andIfSet("CONTACT.PERSON_ID", JSON.parse(vars.getString("$param.BlacklistPersons_param")), SqlBuilder.NOT_IN()) - * - * result.string(cond.toString()); - * - * //////Example 2///// - * var cond = newWhere(); - * - * // note: we can use .and* now without an extra .where - * if (SOMECHECKS) - * cond.andIfSet(...) - * - * if (SOME_MORE_CHECKS) - * cond.and(...) - * - * result.string(cond.toString()); - */ -function newWhereIfSet(pFieldOrCond, pValue, pCondition, pFieldType, pAlias) -{ - return new SqlBuilder(pAlias).whereIfSet(pFieldOrCond, pValue, pCondition, pFieldType); -} + if (!this._where._whereWasCalled) + throw SqlBuilder._ERROR_WHERE_NOT_FIRST(); + + //skip if no values are provided and mandatory is false + if (!pMandatory && pFieldOrCond === undefined && pValue === undefined && pCondition === undefined && pFieldType === undefined) + return this; -/** - * Object for building sqls. The main purpose of this is to make it easy to use prepared statements.<br/> - * You should ALWAYS use prepared statemnts for Security and maybe also for performance reasons.<br/> - * If you are not used to use prepared statements all the time you WILL forget to use it, when it's really needed. (eg. direct user input, not just ids)<br/> - * <br/> - * This can also be useful to build complex sqls where parts should be added<br/> - * dynamically while keeping the code clean.<br/> - * <br/> - * There exist some shortcut funtions<br/> - * - if you need a full select use newSelect(...)<br/> - * - if you need only a condition use newWhere(...) or newWhereIfSet(...) - * - * @param {String} [pAlias=currentAlias] This alias is used for fetching the ColumnTypes and also for the .table, .cell, .updateData, ... -functions - * @class - */ -function SqlBuilder (pAlias) -{ - if(!(this instanceof SqlBuilder)) - throw SqlBuilder._ERROR_INSTANCIATE_WITH_NEW(); - this._select = null; - this._from = null; - this._tableName = null; //for insert/update/delete - this._joins = []; - this._groupBy = null; - this._having = null; - this._orderBy = null; - this._unions = []; - this.alias = pAlias; + if (pFieldOrCond === undefined && pValue === undefined && pCondition === undefined && pFieldType === undefined) + throw SqlBuilder._ERROR_NO_PARAMETER_PROVIDED(); + + // Special case: if only pFieldOrCond is set and we can identify it as a valid field-string (e.g. "Table.Field") we assume that it is not just a condition string. + // --> we can check pValue for undefined and also allow simple string-conditions + // --> this only works if isFullFieldQualifier() can detect if the supplied string is a valid field-string or if it is some sql. + // currently it checks for some special characters which should not exist in any field-string but in conditions. + // If there is a special case missing -> add it to the regexp in isFullFieldQualifier() + if (pValue === undefined && pCondition === undefined && pFieldType === undefined && typeof pFieldOrCond == "string" && SqlUtils.isFullFieldQualifier(pFieldOrCond)) + { + if (pMandatory) + throw SqlBuilder._ERROR_VALUE_IS_MANDATORY(); + + return this; + } + + // just call the andCondition function if it is only a Condition + if (pFieldOrCond !== undefined && pValue === undefined && pCondition === undefined && pFieldType === undefined) + { + return this._addWhereCondition(pFieldOrCond, pMandatory, pAddPreparedConditionCallback, true); + } - //for paging - this._startRow = null; - this._pageSize = null; - this._hasMoreRows = true; + // Subselects containing full select can be used as field, if pValue and pFieldType are provided. + 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._addWhereCondition(tmpCond, pMandatory, pAddPreparedConditionCallback, true) + return this; + } - this._subselectAlias = null; + // 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(); - this._where = {}; - this._initWhere(); -} + // 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 (e.g. with "exists ?") + if ((typeof pFieldOrCond == "string" || Array.isArray(pFieldOrCond) || (!pFieldOrCond && pFieldType)) + || (!pFieldOrCond && (pCondition && pValue instanceof SqlBuilder || !(pValue instanceof SqlBuilder)))) + { + var field = pFieldOrCond; + var typeofValue = typeof pValue; -/** - * @return {Symbol} - */ -SqlBuilder.getCanBuildSqlSymbol = function () -{ - return Symbol["for"]("canBuildSql"); -} + // ... a string starting with $ -> jdito varable which has to be resolved + if (typeofValue == "string" && pValue.length >= 2 && pValue[0] == "$" && pValue[1] != "$") // escape $ if using two $ + { + //important: just overwrite the value because some $local variables may contain an array and then the default handling of arrays (which + //is generating an IN-statement) should apply + pValue = vars.get(pValue); + if (pMandatory && pValue === null) + throw SqlBuilder._ERROR_VALUE_IS_MANDATORY_JDITO_VAR(); + typeofValue = typeof pValue; + } -SqlBuilder.defineCanBuildSql = function (pObject) -{ - pObject[SqlBuilder.getCanBuildSqlSymbol()] = true; -} + // remove the first $ if there are two $ + if (typeofValue == "string" && pValue.length >= 2 && pValue[0] == "$" && pValue[1] == "$") + pValue = pValue.slice(1); + + //support for Set by converting to Array + if (pValue instanceof Set) + pValue = Array.from(pValue); + + // pValue can be... + // ... a SqlBuilder / Prepared statement array -> it is a SqlBuilder containing a complete subquery or an simple array (in statement) + if (pValue instanceof SqlBuilder || Array.isArray(pValue) || (typeofValue == "string" && (pFieldOrCond == undefined || pFieldOrCond == null))) + { + // check if the array is really a value-array for an in and not a prepared statement + if (Array.isArray(pValue) && !SqlUtils.isPreparedSqlArray(pValue)) + { + if (pValue.length == 0) + { + if (pMandatory) + throw SqlBuilder._ERROR_VALUE_IS_MANDATORY(); + + return this; + } + + // if it is null -> ignore it. -> the pCondition should not contain a # in this case + if (field != null) + { + var [alias, parsedField] = SqlUtils.parseField(field); + if (pFieldType === undefined || pFieldType === null) + pFieldType = SqlUtils.getSingleColumnType(parsedField, undefined, this.alias); + } + //overwrite condition to set a default behaviour + if (pCondition == undefined) + pCondition = SqlBuilder.IN(); + // value-array -> convert it to a prepared statement ["(?, ?, ?)", [[val1, type1], [val2, type2], [val3, type3]]] + + var mergeOp = pCondition == SqlBuilder.NOT_IN() ? " and " : " or "; + var inCondition = ArrayUtils.chunk(pValue, 1000) + .map(function (values) + { + return this._prepare(field, SqlUtils.getSqlInStatement(undefined, values, undefined, true, pFieldType), pCondition, pFieldType, false); + }, this) + .reduce(function (fullCondition, nextCondition) + { + return [ + fullCondition[0] + mergeOp + nextCondition[0], + fullCondition[1].concat(nextCondition[1]) + ]; + }); + + this._addWhereCondition(inCondition, undefined, pAddPreparedConditionCallback, true); + return this; + } + + if (pFieldOrCond !== null && pFieldOrCond !== undefined) + { + if (!pCondition) + pCondition = SqlBuilder.EQUAL(); + + pCondition = SqlUtils.replaceConditionTemplate(pCondition, '#', SqlUtils.parseField(pFieldOrCond)[0]); + } + else if (!pCondition) + { + pCondition = "?"; + } + + // _addWhereSubquery can handle SqlBuilder and prepared statements as value + return this._addWhereSubquery(pValue, pMandatory, pCondition, pAddPreparedConditionCallback); + } -SqlBuilder.checkCanBuildSql = function (pObject) -{ - return pObject[SqlBuilder.getCanBuildSqlSymbol()]; -} + if (!pCondition) + pCondition = SqlBuilder.EQUAL(); -SqlBuilder.defineCanBuildSql(SqlBuilder.prototype); + // ... everything else -> just pass it + if (pValue === false || pValue === 0 || pValue === "" || pValue) + { + let prep = this._prepare(field, pValue, pCondition, pFieldType) + this._addWhereCondition(prep, undefined, pAddPreparedConditionCallback); + } + return this; + } + + throw SqlBuilder._ERROR_UNSUPPORTED_PARAMETER_COMBINATION(); +} /** - * Deep copies the SqlBuilder object and returns a new one.<br/> - * Use this if you want to add for example add additional parameters without modifying the current builder. - * @return a full copy of the current SqlBuilder + * helper function that checks if the format of a condition is valid + * + * @param {String} pCondition condition + * @return {String} the given condition, if the format is correct + * @throws when the format is invalid */ -SqlBuilder.prototype.copy = function() +SqlBuilder.prototype._verifyConditionFormat = function (pCondition) { - return Utils.clone(this); -} + if (!pCondition) + { + return pCondition; + } + + //In a special case, pCondition can be a function. It will be called with the alias as argument and + //must return an array of the condition string and (optionally) the required sql field type. + //alternatively the function may return a string only to make the usage more bulletproof and convenient, so both SqlBuilder.EQUAL() + //and SqlBuilder.EQUAL work equally + if (typeof pCondition === "function") + { + var resCond = pCondition(this.alias); + if (Array.isArray(resCond)) + { + pCondition = resCond[0]; + pFieldType = pFieldType || resCond[1]; + } + else if(Utils.isString(pCondition)) + { + pCondition = resCond; + } + } + + // 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 (it uses text.replaceAll which supports lookbehind because it uses java) + var conditionFormat = SqlUtils.replaceConditionTemplate(pCondition, "#", "{@NUMBERSIGN@}") + conditionFormat = SqlUtils.replaceConditionTemplate(conditionFormat, "\\?", "{@QUESTIONSIGN@}") -// errors which are thrown by the SqlBuilder -SqlBuilder._ERROR_INSTANCIATE_WITH_NEW = function() -{ - return new Error(translate.text("SqlBuilder must be instanciated with 'new' or one of the factory methods (newSelect, newWhere, newWhereIfSet)")); + var indexOfNumberSign = conditionFormat.indexOf("{@NUMBERSIGN@}"); + var indexOfQuestionSign = conditionFormat.indexOf("{@QUESTIONSIGN@}"); + + var isFormatValid = !(indexOfQuestionSign == -1 + || indexOfNumberSign > indexOfQuestionSign + || indexOfNumberSign != conditionFormat.lastIndexOf("{@NUMBERSIGN@}") + || indexOfQuestionSign != conditionFormat.lastIndexOf("{@QUESTIONSIGN@}")); + + if (!isFormatValid) + { + throw SqlBuilder._ERROR_CONDITION_WRONG_FORMAT(); + } + + return pCondition; } -SqlBuilder._ERROR_INVALID_CONDITION_VALUE_TYPE = function() +/** + * helper function to add a condition via "and" + * + * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} pFieldOrCond see .where() + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} pValue see .where() + * @param {Boolean} [pMandatory=true] if true: throw error if pValue is null, undefined, SqlBuilder without condition, etc... else just ignore the condition + * @param {String} [pCondition="# = ?"] see .where() + * @param {SQLTYPES|Numeric} [pFieldType=AutomaticallyLoadedType] see .where() + * + * @ignore + */ +SqlBuilder.prototype._and = function(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType) { - return new Error(translate.text("SqlBuilder: invalid value-type for pCondition")); + return this._addWhere(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType, function(pPreparedCondition) + { + this._where._previouslyOnlyOr = false; + if (SqlUtils.isNonEmptyPreparedSqlArray(pPreparedCondition)) + { + if (this.hasCondition()) + this._where.sqlStorage += " and "; + + this._where.sqlStorage += pPreparedCondition[0]; + this._where.preparedValues = this._where.preparedValues.concat(pPreparedCondition[1]); + } + }); } -SqlBuilder._ERROR_NO_CONDITION = function() -{ - 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: 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: 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)")); +/** + * helper function to add a condition via "or" + * The callback inside of this function adds brackets where needed. + * + * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} pFieldOrCond see .where() + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} pValue see .where() + * @param {Boolean} [pMandatory=true] if true: throw error if pValue is null, undefined, SqlBuilder without condition, etc... else just ignore the condition + * @param {String} [pCondition="# = ?"] see .where() + * @param {SQLTYPES|Numeric} [pFieldType=AutomaticallyLoadedType] see .where() + * + * @ignore + */ +SqlBuilder.prototype._or = function(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType) +{ + return this._addWhere(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType, function(pPreparedCondition, pAlreadySurroundedByBrackets) + { + if (SqlUtils.isNonEmptyPreparedSqlArray(pPreparedCondition)) + { + if (this._where._previouslyOnlyOr) + { + this._where.sqlStorage = this._where.sqlStorage + " or " + pPreparedCondition[0]; + this._where._lastWasOr = true; + } + else if (this.hasCondition()) + { + let cond = pPreparedCondition[0]; + + if (!pAlreadySurroundedByBrackets) + cond = "(" + cond + ")"; + + if (this._where._lastWasOr) + this._where.sqlStorage = this._where.sqlStorage + " or " + cond; + else + this._where.sqlStorage = "(" + this._where.sqlStorage + ") or " + cond; + + this._where._lastWasOr = true; + } + else + { + if (!this.hasCondition()) + this._where._previouslyOnlyOr = true; + + this._where.sqlStorage = pPreparedCondition[0]; + } + this._where.preparedValues = this._where.preparedValues.concat(pPreparedCondition[1]); + } + }); } -SqlBuilder._ERROR_VALUE_IS_MANDATORY_JDITO_VAR = function() +/** + * Constant-like function which provides a value for pCondition if you need a "not" statement. + * + * @return {String} + * + * @example + * var cond = newWhere(null, someCondition, SqlBuilder.NOT()) + */ +SqlBuilder.NOT = function() { - 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)")); + return "not ?"; } -SqlBuilder._ERROR_UNSUPPORTED_PARAMETER_COMBINATION = function() +/** + * Constant-like function which provides a value for pCondition if you need a "not in" statement. + * + * @return {String} + * + * @example + * var cond = newWhere("PERSON.FIRSTNAME", ["Fritz"], SqlBuilder.NOT_IN()) + */ +SqlBuilder.NOT_IN = function() { - return new Error(translate.text("SqlBuilder: unsupportet parameter combination")); + return "# not in ?"; } -SqlBuilder._ERROR_NO_TABLE = function() +/** + * Constant-like function which provides a value for pCondition if you need a "in" statement. + * + * @return {String} + * + * @example + * var cond = newWhere("PERSON.FIRSTNAME", ["Fritz"], SqlBuilder.IN()) + */ +SqlBuilder.IN = function() { - return new Error(translate.text("SqlBuilder.deleteDat/updateData: You have to specify a tablename")); + return "# in ?"; } -SqlBuilder._ERROR_NO_PARAMETER_PROVIDED = function() +/** + * Constant-like function which provides a value for pCondition if you need a "exists" statement. + * + * @return {String} + * + * @example + * var cond = newWhere(null, mySubSqlBuilder, SqlBuilder.EXISTS()) + */ +SqlBuilder.EXISTS = function() { - return new Error(translate.text("SqlBuilder: You have to specify at least one parameter")); + return "exists ?"; } -SqlBuilder._ERROR_WHERE_NOT_FIRST = function() +/** + * Constant-like function which provides a value for pCondition if you need a "not exists" statement. + * + * @return {String} + * + * @example + * var cond = newWhere(null, mySubSqlBuilder, SqlBuilder.NOT_EXISTS()) + */ +SqlBuilder.NOT_EXISTS = function() { - return new Error(translate.text("SqlBuilder: .where has to be called before following and/or.")); + return "not exists ?"; } -SqlBuilder._ERROR_ONLY_ONE_WHERE = function() +/** + * Constant-like function which provides a value for pCondition if you need a "year(#) = ?" statement. + * If you use this, the default pFieldType will be SQLTYPES.INTEGER. + * + * @return {Function} + * + * @example + * var cond = newWhere("FORECAST.DATE_START", DateUtils.getCurrentYear(), SqlBuilder.YEAR_EQUALS()); + */ +SqlBuilder.YEAR_EQUALS = function () { - return new Error(translate.text("SqlBuilder: .where has to be called only one time. Use and/or for further conditions.")); + //function will be called later so it can use the alias of the SqlBuilder + return function (pAlias) {return [(new SqlMaskingUtils(pAlias).yearFromDate("#")) + " = ?", SQLTYPES.INTEGER];}; } -SqlBuilder._ERROR_INCOMPLETE_SELECT = function () +/** + * Constant-like function which provides a value for pCondition if you need a "# = ?" statement. + * This is the default for the pCondition parameter, so it can be omitted. + * + * @return {String} + * + * @example + * var cond = newWhere("PERSON.FIRSTNAME", "Fritz", SqlBuilder.EQUAL()) + */ +SqlBuilder.EQUAL = function () { - return new Error(translate.text("SqlBuilder: select and from were expected, but not provided.")); + return "# = ?"; } -SqlBuilder._ERROR_CONDITION_IS_MANDATORY = function () +/** + * Constant-like function which provides a value for pCondition if you need a "# <> ?" statement. + * + * @return {String} + * + * @example + * var cond = newWhere("PERSON.FIRSTNAME", "Fritz", SqlBuilder.NOT_EQUALS()) + */ +SqlBuilder.NOT_EQUAL = function () { - return new Error(translate.text("SqlBuilder: You have to provide a subquery as SqlBuilder, prepared-array or string")); + return "# <> ?"; } -SqlBuilder._ERROR_SUBSELECT_AS_FIELD_NOT_COMPLETE = function () +/** + * Constant-like function which provides a value for pCondition if you need a "# like ?" statement. + * + * @return {String} + * + * @example + * var cond = newWhere("PERSON.FIRSTNAME", "F%", SqlBuilder.LIKE()) + */ +SqlBuilder.LIKE = 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")); + return "# like ?"; } -SqlBuilder._ERROR_SUBSELECT_AS_FIELD_NO_FIELD_TYPE = function () +/** + * Constant-like function which provides a value for pCondition if you need a "# like ?" statement. + * + * @return {String} + * + * @example + * var cond = newWhere("PERSON.FIRSTNAME", "F%", SqlBuilder.NOT_LIKE()) + */ +SqlBuilder.NOT_LIKE = 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")); + return "# not like ?"; } -SqlBuilder._ERROR_CONDITION_WRONG_FORMAT = function () +/** + * Constant-like function which provides a value for pCondition if you need a "# > ?" statement. + * + * @return {String} + */ +SqlBuilder.GREATER = 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.")); + return "# > ?"; } -SqlBuilder._ERROR_NOT_BOOLEAN = function () +/** + * Constant-like function which provides a value for pCondition if you need a "# < ?" statement. + * + * @return {String} + */ +SqlBuilder.LESS = function () { - return new Error(translate.text("pExecuteOnlyIfConditionExists has to be of type boolean. This parameter controls what happens if the condition is empty (select / delete all or nothing)")); + return "# < ?"; } -SqlBuilder._ERROR_UPDATE_VALUES_INVALID = function () +/** + * Constant-like function which provides a value for pCondition if you need a "# >= ?" statement. + * + * @return {String} + */ +SqlBuilder.GREATER_OR_EQUAL = function () { - return new Error(translate.text("SqlBuilder: The provided values object for updateFields is invalid or is not an object.")); + return "# >= ?"; } -SqlBuilder._ERROR_PAGESIZE_INVALID = function () +/** + * Constant-like function which provides a value for pCondition if you need a "# <= ?" statement. + * + * @return {String} + */ +SqlBuilder.LESS_OR_EQUAL = function () { - return new Error(translate.text("SqlBuilder: The pagesize is not set or is not a number.")); + return "# <= ?"; } -SqlBuilder._ERROR_NOT_A_FUNCTION = function () -{ - return new Error(translate.text("SqlBuilder: The provided callback function is not a function.")); -} /** - * Alternative way of creating a new SqlBuilder object that allows to use - * methods on it directly without having to put brackets around it - * - * @return {SqlBuilder} a new SqlBuilder object - * - * @example - * var query = SqlBuilder.begin() - * .select("ORGANISATION.NAME, FIRSTNAME, LASTNAME") - * .from("PERSON") - * .join("CONTACT", "CONTACT.PERSON_ID = PERSON.PERSONID") - * .leftJoin("ORGANISATION", SqlCondition.begin() - * .and("CONTACT.ORGANISATION_ID = ORGANISATION.ORGANISATIONID") - * .andPrepare("ORGANISATION.NAME", "S%", "# like ?") - * .build("1=2")) - * .where(SqlCondition.begin() - * .andPrepare("CONTACT.STATUS", $KeywordRegistry.contactStatus$active()) - * .build("1=2")); - * - * if (getCountry) //changing and adding parts - * { - * query.select("ORGANISATION.NAME, FIRSTNAME, LASTNAME, COUNTRY"); - * query.leftJoin("ADDRESS", "CONTACT.ADDRESS_ID = ADDRESS.ADDRESSID"); - * } - * - * var data = db.table(query.build()); + * Constant-like function which returns an impossible condition ("1 = 2"). * - * @deprecated using .begin is deprecated as it's now possible to write "new SqlBuilder().select(...).from(...).... - You can now use "newSelect(...)", "newWhere(...)", "newWhereIfSet(...)" or "new SqlBuilder()" to create a new SqlBuilder instance. - For further SqlBuilder usage see the documentation-property of the Sql_lib. + * @return {String} */ -SqlBuilder.begin = function () +SqlBuilder.NORESULT_CONDITION = function () { - return new SqlBuilder(); + return "1 = 2"; } - /** - * Builds the sql and uses SqlUtils.translateXXXWithQuotes to make a string out of it. - * @param {String} [pDefaultConditionIfNone=""] the default condition string to use if the SqlBuilder contains no condition. In most cases you won't need this - * @param {Boolean} [pForceAsStatement=false] forces the use of SqlUtils.translateStatementWithQuotes even if it's no full statement. This is needed for example if you do not want brakets around the generated statement - * @return {String} the sql as string + * Object providing constant-like functions for sql-any-conditions. */ -SqlBuilder.prototype.toString = function(pDefaultConditionIfNone, pForceAsStatement) -{ - var built = this.build(pDefaultConditionIfNone) - - if (built[0] !== "") - { - if (!pForceAsStatement && !this.isFullSelect() && (this.hasCondition() || pDefaultConditionIfNone)) - return SqlUtils.translateConditionWithQuotes(built, this.alias); - else - return SqlUtils.translateStatementWithQuotes(built, this.alias); - } - - return ""; -} - -/** - * Sets the select clause of the sql. - * @param {String|Array|SqlBuilder} pFields You can pass:<br/> - * - A String is just used AS IT IS. (e.g. "FIRSTNAME, LASTNAME")<br/> - * - SqlBuilder is used as Subquery<br/> - * - The array can also contain Strings, SqlBuilder which are just concatenated (e.g. ["FIRSTNAME", "LASTNAME", someSqlBuilderContainingFullSelect]) - * - * @return {SqlBuilder} current SqlBuilder object - */ -SqlBuilder.prototype.select = function(pFields) -{ - this._select = SqlBuilder._getStatement(pFields, "select", undefined, true, true); - return this; -} - -/** - * Sets the select clause of the sql with distinct. - * @param {String|String[]} pFields - * @return {SqlBuilder} current SqlBuilder object - */ -SqlBuilder.prototype.selectDistinct = function (pFields) -{ - this._select = SqlBuilder._getStatement(pFields, "select distinct", undefined, true, true); - return this; -} - -/** - * Sets the select clause to "select count(...)" - * @param {String} [pField=*] sql column to count, if omitted "count(*)" will be used - * @return {SqlBuilder} current SqlBuilder object - */ -SqlBuilder.prototype.selectCount = function (pField) -{ - if (pField == undefined) - { - pField = "*"; - } - this._select = SqlBuilder._getStatement(pField, "select count(", ")", true, true); - return this; -} - - -/** - * sets an alias-name which is added at some places if this SqlBuilder is used as subselect (e.g. in .select(), .join(), .from(), ...) - * @param {String} pSubselectAlias - * - * @return {SqlBuilder} current SqlBuilder object - */ -SqlBuilder.prototype.subselectAlias = function(pSubselectAlias) -{ - this._subselectAlias = pSubselectAlias; - return this; -} - -/** - * Sets the table that is used for insert/update/delete functions. - * - * @param {String} pTable - * @return {SqlBuilder} current SqlBuilder object - */ -SqlBuilder.prototype.tableName = function (pTable) -{ - this._tableName = pTable; - return this; -} - -/** - * Sets the from clause of the sql.<br/> - * <br/> - * Note: It is recommended to add joins via the .join functions.<br/> - * But in some cases you may already get a full from clause including the joins. In this case it is also possible to include them in the from-string.<br/> - * - * @param {String|SqlBuilder} pTable if it is a String, it is used as it is as table<br/> - * if it is a SqlBuilder, it is used as subselect: e.g. select * from (select FIRSTNAME from PERSON) - * @param {String} [pTableAlias] table alias - * @return {SqlBuilder} current SqlBuilder object - */ -SqlBuilder.prototype.from = function(pTable, pTableAlias) -{ - this._from = SqlBuilder._getStatement(pTable, "", pTableAlias, false, (pTableAlias ? false : true)); - if (typeof(pTable) === "string") - this._tableName = pTable; - return this; -} - -/** - * Adds a join clause to the sql. - * @param {String|SqlBuilder} pTable if it is a String, it is used as it is as table<br/> - * if it is a SqlBuilder, it is used as subselect: e.g. select * from Table1 join (select FIRSTNAME from PERSON) on ... - * @param {String|SqlBuilder} [pCondition] The where condition. This can be<br/> - * - a string (without the where keyword)<br/> - * - a SqlBuilder NOTE: only the condition is used from it - * - * @param {String} [pTableAlias] This alias is used to add an alias to the tablename - * @param {String} [pPrefix] string before the join, for example "left", "right" - * @param {String} [pReplacementForWordJoin] if this is set, this is used instead of the word "join". Needed for e.g. OUTER APPLY in MSSQL - * @return {SqlBuilder} current SqlBuilder object - */ -SqlBuilder.prototype.join = function(pTable, pCondition, pTableAlias, pPrefix, pReplacementForWordJoin) -{ - // support for deprecated SqlCondition - if (pCondition instanceof SqlCondition) - { - pCondition = newWhere(pCondition); - logging.log("Warning: using .where with a SqlCondition as pFieldOrCond is deprecated. The SqlCondition will be removed in version >= 2020.x\n" - + "For SqlBuilder usage see the documentation-property of the Sql_lib."); - } - - var prefix = (pReplacementForWordJoin ? pReplacementForWordJoin : "join"); - if (pPrefix) - prefix = pPrefix + " " + prefix; - - var postfix = pCondition ? "on" : ""; - - if (pTableAlias) - postfix = pTableAlias + " " + postfix; - else if (pTable instanceof SqlBuilder && pTable._subselectAlias) - postfix = pTable._subselectAlias + " " + postfix; - - var joinPart = SqlBuilder._getStatement(pTable, prefix, postfix.trim()); - - if (pCondition) - { - if (pCondition instanceof SqlBuilder) - pCondition = [pCondition._where.sqlStorage, pCondition._where.preparedValues] - - var conditionPart = SqlBuilder._getStatement(pCondition); - - joinPart.sqlStorage += " " + conditionPart.sqlStorage; - joinPart.preparedValues = joinPart.preparedValues.concat(conditionPart.preparedValues); - } - - this._joins.push(joinPart) - return this; -} - -/** - * Adds a left join clause to the sql. - * - * @param {String|SqlBuilder} pTable if it is a String, it is used as it is as table<br/> - * if it is a SqlBuilder, it is used as subselect: e.g. select * from Table1 join (select FIRSTNAME from PERSON) on ... - * @param {String|SqlBuilder} [pCondition] The where condition. This can be<br/> - * - a string (without the where keyword)<br/> - * - a SqlBuilder NOTE: only the condition is used from it - * - * @param {String} [pTableAlias] This alias is used to add an alias to the tablename - * @return {SqlBuilder} current SqlBuilder object - */ -SqlBuilder.prototype.leftJoin = function(pTable, pCondition, pTableAlias) -{ - return this.join(pTable, pCondition, pTableAlias, "left"); +SqlBuilder.ANY = { + /** + * Constant-like function that returns a "# = any ?" statement. + */ + EQUAL : function () {return "# = any ?";}, + /** + * Constant-like function that returns a "# <> any ?" statement. + */ + NOT_EQUAL : function () {return "# <> any ?";}, + /** + * Constant-like function that returns a "# > any ?" statement. + */ + GREATER : function () {return "# > any ?";}, + /** + * Constant-like function that returns a "# >= any ?" statement. + */ + GREATER_OR_EQUAL : function () {return "# >= any ?";}, + /** + * Constant-like function that returns a "# < any ?" statement. + */ + LESS : function () {return "# < any ?";}, + /** + * Constant-like function that returns a "# <= any ?" statement. + */ + LESS_OR_EQUAL : function () {return "# <= any ?";} } /** - * Adds a right join clause to the sql. - * - * @param {String|SqlBuilder} pTable if it is a String, it is used as it is as table<br/> - * if it is a SqlBuilder, it is used as subselect: e.g. select * from Table1 join (select FIRSTNAME from PERSON) on ... - * @param {String|SqlBuilder} [pCondition] The where condition. This can be<br/> - * - a string (without the where keyword)<br/> - * - a SqlBuilder NOTE: only the condition is used from it - * - * @param {String} [pTableAlias] This alias is used to add an alias to the tablename - * @return {SqlBuilder} current SqlBuilder object + * Object providing constant-like functions for sql-all-conditions. */ -SqlBuilder.prototype.rightJoin = function(pTable, pCondition, pTableAlias) -{ - return this.join(pTable, pCondition, pTableAlias, "right"); +SqlBuilder.ALL = { + /** + * Constant-like function that returns a "# = all ?" statement. + */ + EQUAL : function () {return "# = all ?";}, + /** + * Constant-like function that returns a "# <> all ?" statement. + */ + NOT_EQUAL : function () {return "# <> all ?";}, + /** + * Constant-like function that returns a "# > all ?" statement. + */ + GREATER : function () {return "# > all ?";}, + /** + * Constant-like function that returns a "# >= all ?" statement. + */ + GREATER_OR_EQUAL : function () {return "# >= all ?";}, + /** + * Constant-like function that returns a "# < all ?" statement. + */ + LESS : function () {return "# < all ?";}, + /** + * Constant-like function that returns a "# <= all ?" statement. + */ + LESS_OR_EQUAL : function () {return "# <= all ?";} } /** * Throws an error if pValue is null, undefined or a SqlBuilder without condition (or if pValue is a $-variable: error if the result of it is null or undefined)<br/> * Also throws an error if pFieldOrCond is the only parameter and it is null<br/> * <br/> - * Starts the where clause of the SQL. You may pass the first condition with it.<br/> - * But you can also call this function without any parameter and add the conditions with subsequent .and / .or<br/> - * <br/> - * This method exists mainly for semantic reasons and can only be callled once.<br/> - * As shourtcut you could use the newWhere(...) function.<br/> + * Adds a condition by using "or" to the Sql.<br/> + * Note: You have to call .where before using .and / .or (this is mainly for semantic reasons) * * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] If this is the only parameter, it is used as Condition <br/> * else it is used as Field.<br/> @@ -1187,9 +1383,6 @@ SqlBuilder.prototype.rightJoin = function(pTable, pCondition, pTableAlias) * Note1: you may have problems with names containing a '.' Use the next variant (as array) in this case<br/> * Note2: if you need a table alias use the next variant (as array)<br/> * - a array: ["TABLENAME", "COLUMNNAME", "tableAlias"] OR ["TABLENAME", "COLUMNNAME"]<br/> - * - a SqlBuilder: the full select is used as subselect and compared with pValue. <br/> - * (e.g. select * from PERSON where (select "NAME" from ORGANISATION where ... ) = ?)<br/> - * Note: for this you have to provide pFieldType as the type cannot be calculated from the subselect!<br/> * Note: this can also be null if you don't need the field and use a pCondition without a # * * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value which is used for the condition.<br/> @@ -1200,8 +1393,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<br/> - * <strong>IMPORTANT: the # has to be before the ?</strong> + * ? will be replaced by pValue + * * @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/> @@ -1209,34 +1402,16 @@ SqlBuilder.prototype.rightJoin = function(pTable, pCondition, pTableAlias) * * @return {SqlBuilder} current SqlBuilder object */ -SqlBuilder.prototype.where = function(pFieldOrCond, pValue, pCondition, pFieldType) -{ - // support for deprecated SqlCondition - if (pFieldOrCond instanceof SqlCondition && pValue === undefined && pCondition === undefined && pFieldType === undefined) - { - let copiedCondition = newWhere(); - - copiedCondition._where.preparedValues = pFieldOrCond.preparedValues; - copiedCondition._where._lastWasOr = pFieldOrCond._lastWasOr; - copiedCondition._where.sqlStorage = pFieldOrCond.sqlStorage; - - pFieldOrCond = copiedCondition; - - logging.log("Warning: using .where with a SqlCondition as pFieldOrCond is deprecated. The SqlCondition will be removed in version >= 2020.x\n" - + "For SqlBuilder usage see the documentation-property of the Sql_lib."); - } - - return this._setWhere(pFieldOrCond, pValue, pCondition, pFieldType, this.or); -} - +SqlBuilder.prototype.or = function(pFieldOrCond, pValue, pCondition, pFieldType) +{ + return this._or(pFieldOrCond, pValue, true, pCondition, pFieldType); +} + /** - * Difference to where(): where throws errors on invalid values, whereIfSet just ignores the condition and does nothing (usefull e.g. for the parameter variables ("$param.ddd") in conditionProcesses.)<br/> - * <br/> - * Starts the whereIfSet clause of the SQL. You may pass the first condition with it.<br/> - * But you can also call this function without any parameter and add the conditions with subsequent .and / .or<br/> + * Difference to or(): or() throws errors on invalid values, orIfSet just ignores the condition and does nothing (usefull e.g. for the parameter variables ("$param.ddd") in conditionProcesses.)<br/> * <br/> - * This method exists mainly for semantic reasons and can only be callled once.<br/> - * As shourtcut you could use the newWhereIfSet(...) function. + * Adds a condition by using "or" to the Sql.<br/> + * Note: You have to call .where before using .and / .or (this is mainly for semantic reasons) * * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] If this is the only parameter, it is used as Condition <br/> * else it is used as Field.<br/> @@ -1254,9 +1429,6 @@ SqlBuilder.prototype.where = function(pFieldOrCond, pValue, pCondition, pFieldTy * Note1: you may have problems with names containing a '.' Use the next variant (as array) in this case<br/> * Note2: if you need a table alias use the next variant (as array)<br/> * - a array: ["TABLENAME", "COLUMNNAME", "tableAlias"] OR ["TABLENAME", "COLUMNNAME"]<br/> - * - a SqlBuilder: the full select is used as subselect and compared with pValue. <br/> - * (e.g. select * from PERSON where (select "NAME" from ORGANISATION where ... ) = ?)<br/> - * Note: for this you have to provide pFieldType as the type cannot be calculated from the subselect!<br/> * Note: this can also be null if you don't need the field and use a pCondition without a # * * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value which is used for the condition.<br/> @@ -1267,8 +1439,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> + * ? will be replaced by pValue * * @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/> @@ -1277,3006 +1448,2201 @@ SqlBuilder.prototype.where = function(pFieldOrCond, pValue, pCondition, pFieldTy * * @return {SqlBuilder} current SqlBuilder object */ -SqlBuilder.prototype.whereIfSet = function(pFieldOrCond, pValue, pCondition, pFieldType) +SqlBuilder.prototype.orIfSet = function(pFieldOrCond, pValue, pCondition, pFieldType) { - return this._setWhere(pFieldOrCond, pValue, pCondition, pFieldType, this.orIfSet); + return this._or(pFieldOrCond, pValue, false, pCondition, pFieldType); } /** - * helper function for .where and .whereIfSet because they do almost the same<br/> - * See .where() for further explanations + * Throws an error if pValue is null, undefined or a SqlBuilder without condition (or if pValue is a $-variable: error if the result of it is null or undefined)<br/> + * Also throws an error if pFieldOrCond is the only parameter and it is null<br/> + * <br/> + * Adds a condition by using "and" to the Sql.<br/> + * Note: You have to call .where before using .and / .or (this is mainly for semantic reasons) * - * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] - * @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() - * + * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] If this is the only parameter, it is used as Condition <br/> + * else it is used as Field.<br/> + * <br/> + * If you use it as Subselect (the only param), it can be:<br/> + * - a string: just added as it is<br/> + * - a PreparedSqlArray: an Array in this form: [sqlStr, [[value1, type1], [valueN, typeN]]]<br/> + * the sql is just used as it is.<br/> + * - a SqlBuilder: ONLY THE CONDITION is used from it<br/> + * <br/> + * If you use it as a Field (at least pValue has to be filled), this param provides the field information to<br/> + * load the SQLTYPE for this condition. <br/> + * It can be provided in the following ways:<br/> + * - a string: ONLY in this form: "TABLENAME.COLUMNNAME" <br/> + * Note1: you may have problems with names containing a '.' Use the next variant (as array) in this case<br/> + * Note2: if you need a table alias use the next variant (as array)<br/> + * - a array: ["TABLENAME", "COLUMNNAME", "tableAlias"] OR ["TABLENAME", "COLUMNNAME"]<br/> + * Note: this can also be null if you don't need the field and use a pCondition without a # + * + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value which is used for the condition.<br/> + * Basically it can be nearly everything you need.<br/> + * - String, etc: is just used as value for the prepared statement. Of course it has to fit the type of the db-column<br/> + * - String starting with '$' is treated as jdito-variable: is loaded with vars.getString("$..."). <br/> + * Note: Use 2 '$' to escape the $ if you don't want it to be treated as JditoVar + * + * @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 + * + * @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/> + * then the db-field is DATETIME, but the value is INTEGER. In this case you can overwrite the type. + * * @return {SqlBuilder} current SqlBuilder object - * @ignore */ -SqlBuilder.prototype._setWhere = function (pFieldOrCond, pValue, pCondition, pFieldType, pAddCondFn) +SqlBuilder.prototype.and = function(pFieldOrCond, pValue, pCondition, pFieldType) { - // allow where-call without parameter to just enable where mode - if (pFieldOrCond === undefined && pValue === undefined && pCondition === undefined && pFieldType === undefined) - { - this._where._whereWasCalled = true; - return this; - } - - // where has to be called before all other and / or - if (this.hasCondition()) - throw SqlBuilder._ERROR_WHERE_NOT_FIRST(); - - // only one where call is allowed - if (this._where._whereWasCalled) - throw SqlBuilder._ERROR_ONLY_ONE_WHERE(); - - this._where._whereWasCalled = true; - return pAddCondFn.call(this, pFieldOrCond, pValue, pCondition, pFieldType); + return this._and(pFieldOrCond, pValue, true, pCondition, pFieldType); } /** - * helper function which adds a condition + * Difference to and(): and() throws errors on invalid values, andIfSet just ignores the condition and does nothing (usefull e.g. for the parameter variables ("$param.ddd") in conditionProcesses.)<br/> + * <br/> + * Adds a condition by using "and" to the Sql.<br/> + * Note: You have to call .where before using .and / .or (this is mainly for semantic reasons) * - * @param {String|SqlBuilder|PreparedSqlArray} pCondition the condition to add - * @param {Boolean} [pMandatory=true] if true: throws error on SqlBuilder without conditon or PreparedSqlArray with empty string. Else: just does nothing - * @param {CallbackFunction} pAddPreparedConditionCallback A Callback funtion which receives a PreparedSqlArray as parameter - * @param {Boolean} pBrackets if true, Brackets are added in some cases + * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] If this is the only parameter, it is used as Condition <br/> + * else it is used as Field.<br/> + * <br/> + * If you use it as Subselect (the only param), it can be:<br/> + * - a string: just added as it is<br/> + * - a PreparedSqlArray: an Array in this form: [sqlStr, [[value1, type1], [valueN, typeN]]]<br/> + * the sql is just used as it is.<br/> + * - a SqlBuilder: ONLY THE CONDITION is used from it<br/> + * <br/> + * If you use it as a Field (at least pValue has to be filled), this param provides the field information to<br/> + * load the SQLTYPE for this condition. <br/> + * It can be provided in the following ways:<br/> + * - a string: ONLY in this form: "TABLENAME.COLUMNNAME" <br/> + * Note1: you may have problems with names containing a '.' Use the next variant (as array) in this case<br/> + * Note2: if you need a table alias use the next variant (as array)<br/> + * - a array: ["TABLENAME", "COLUMNNAME", "tableAlias"] OR ["TABLENAME", "COLUMNNAME"]<br/> + * Note: this can also be null if you don't need the field and use a pCondition without a # + * + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value which is used for the condition.<br/> + * Basically it can be nearly everything you need.<br/> + * - String, etc: is just used as value for the prepared statement. Of course it has to fit the type of the db-column<br/> + * - String starting with '$' is treated as jdito-variable: is loaded with vars.getString("$..."). <br/> + * Note: Use 2 '$' to escape the $ if you don't want it to be treated as JditoVar + * + * @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 + * + * @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/> + * then the db-field is DATETIME, but the value is INTEGER. In this case you can overwrite the type. * * @return {SqlBuilder} current SqlBuilder object - * @ignore */ -SqlBuilder.prototype._whereCondition = function(pCondition, pMandatory, pAddPreparedConditionCallback, pBrackets) +SqlBuilder.prototype.andIfSet = function(pFieldOrCond, pValue, pCondition, pFieldType) { - if (pCondition === undefined) - return this; - - if (pMandatory === undefined) - pMandatory = true; - - var sql = pCondition; - var typeofSql = typeof sql; - - // the field is a simple string -> just add the string, no prepared statement - if (typeofSql == "string") - { - pAddPreparedConditionCallback(this, [sql, []]); - return this; - } - - // the field is an array -> it is a prepared condition - if (Array.isArray(sql)) - { - if (sql[0]) - { - this._where.preparedValues = this._where.preparedValues.concat(sql[1]); - - // add only brackets if needed - if (pBrackets) - sql[0] = " ( " + sql[0] + " ) "; - - pAddPreparedConditionCallback(this, [sql[0], []], pBrackets) - return this; - } - else if (pMandatory) - throw SqlBuilder._ERROR_CONDITION_IS_MANDATORY(); - - return this; - } - - // the field is a SqlBuilder -> it is a SqlBuilder which contains a condition -> the condition of the SqlBuilder is added. - if (sql instanceof SqlBuilder) - { - // add only brackets if needed - var sqlString = sql._where.sqlStorage; - - - var condString = sqlString; - if (condString.trim() != "") - { - if (pBrackets) - condString = " ( " + condString + " ) "; - - pAddPreparedConditionCallback(this, [condString, sql._where.preparedValues], pBrackets); - return this; - } - else if (pMandatory) - throw SqlBuilder._ERROR_CONDITION_IS_MANDATORY(); - - return this; - } - - throw SqlBuilder._ERROR_INVALID_CONDITION_VALUE_TYPE(); + return this._and(pFieldOrCond, pValue, false, pCondition, pFieldType); } /** - * helper function which adds a Subquery-condition + * Sets the order by clause of the sql. * - * @param {SqlBuilder|PreparedSqlArray} pSubquery the subquery to add - * @param {Boolean} [pMandatory=true] if true: throws error on SqlBuilder without conditon or PreparedSqlArray with empty string. Else: just does nothing - * @param {Boolean} pCondition the condition to be used: e.g. "exists(?)" the ? is replaced by the subquery - * @param {CallbackFunction} pAddPreparedConditionCallback A Callback funtion which receives a PreparedSqlArray as parameter - * + * @param {String|String[]} pOrderBy a string is added as it is, a array is concatenated by ', ' * @return {SqlBuilder} current SqlBuilder object - * @ignore */ -SqlBuilder.prototype._whereSubquery = function(pSubquery, pMandatory, pCondition, pAddPreparedConditionCallback) +SqlBuilder.prototype.orderBy = function(pOrderBy) { - if (pSubquery === undefined) - return this; - - if (pMandatory === undefined) - pMandatory = true; - - var sql = pSubquery; - - // the field is an array -> it is a prepared statement which already SHOULD contain exists or another condition - // Both can be handled by _prepare - if (Array.isArray(sql)) - { - if (sql[0]) - pAddPreparedConditionCallback(this, this._prepare(undefined, sql, pCondition)); - else if (pMandatory) - throw SqlBuilder._ERROR_VALUE_IS_MANDATORY(); - - return this; - } - - // the field is a SqlBuilder -> it is a SqlBuilder which contains a condition -> the condition of the SqlBuilder is added. - if (sql instanceof SqlBuilder) - { - var subQuery = pSubquery; - - // Without condition this function cannot be used with SqlBuilder object as it cannot contain a condition - if (!pCondition) - throw SqlBuilder._ERROR_NO_CONDITION(); - - if (subQuery.isFullSelect() || subQuery.hasCondition()) //can also be only an condition if SqlBuilder.NOT() is used - { - var preparedObj = subQuery.build(); - pAddPreparedConditionCallback(this, this._prepare(undefined, preparedObj, pCondition)); - } - else if (pMandatory) - throw SqlBuilder._ERROR_VALUE_IS_MANDATORY(); - - return this; - } - - throw SqlBuilder._ERROR_INVALID_SUBQUERY_TYPE(); + this._orderBy = SqlBuilder._getStatement(pOrderBy, "order by", undefined, true); + return this; } /** - * helper function which adds a condition to the where - * - * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} pFieldOrCond see .where() - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} pValue see .where() - * @param {Boolean} [pMandatory=true] if true: throw error if pValue is null, undefined, SqlBuilder without condition, etc... else just ignore the condition - * @param {String} [pCondition="# = ?"] see .where() - * @param {SQLTYPES|Numeric} [pFieldType=AutomaticallyLoadedType] see .where() - * @param {CallbackFunction} pAddPreparedConditionCallback A Callback funtion which receives a PreparedSqlArray as parameter + * Sets the group by clause of the sql. * + * @param {String|String[]} pFields a string is added as it is, a array is concatenated by ', ' * @return {SqlBuilder} current SqlBuilder object - * @ignore */ -SqlBuilder.prototype._addWhere = function(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType, pAddPreparedConditionCallback) +SqlBuilder.prototype.groupBy = function(pFields) { - //In a special case, pCondition can be a function. It will be called with the alias as argument and - //must return an array of the condition string and (optionally) the required sql field type. - //alternatively the function may return a string only to make the usage more bulletproof and convenient, so both SqlBuilder.EQUAL() - //and SqlBuilder.EQUAL work equally - if (pCondition && typeof pCondition === "function") - { - var resCond = pCondition(this.alias); - if (Array.isArray(resCond)) - { - pCondition = resCond[0]; - pFieldType = pFieldType || resCond[1]; - } - else if(Utils.isString(pCondition)) - { - pCondition = resCond; - } - } - - 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(); - - if (!pMandatory && pFieldOrCond === undefined && pValue === undefined && pCondition === undefined && pFieldType === undefined) - return this; - - if (pFieldOrCond === undefined && pValue === undefined && pCondition === undefined && pFieldType === undefined) - throw SqlBuilder._ERROR_NO_PARAMETER_PROVIDED(); - - // Special case: if only pFieldOrCond is set and we can identify it as a valid field-string (e.g. "Table.Field") we assume that it is not just a condition string. - // --> we can check pValue for undefined and also allow simple string-conditions - // --> this only works if isFullFieldQualifier() can detect if the supplied string is a valid field-string or if it is some sql. - // currently it checks for some special characters which should not exist in any field-string but in conditions. - // If there is a special case missing -> add it to the regexp in isFullFieldQualifier() - if (pValue === undefined && pCondition === undefined && pFieldType === undefined && typeof pFieldOrCond == "string" && SqlUtils.isFullFieldQualifier(pFieldOrCond)) - { - if (pMandatory) - throw SqlBuilder._ERROR_VALUE_IS_MANDATORY(); - else - return this; - } - - // 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); - - // Subselects containing full select can be used as field, if pValue and pFieldType are provided. - 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 (e.g. with "exists ?") - if(((typeof pFieldOrCond == "string" || Array.isArray(pFieldOrCond) || (!pFieldOrCond && pFieldType)) || (!pFieldOrCond && (pCondition && pValue instanceof SqlBuilder || !(pValue instanceof SqlBuilder))))) - { - var field = pFieldOrCond; - var typeofValue = typeof pValue; - - // ... a string starting with $ -> jdito varable which has to be resolved - if (typeofValue == "string" && pValue.length >= 2 && pValue[0] == "$" && pValue[1] != "$") // escape $ if using two $ - { - //important: just overwrite the value because some $local variables may contain an array and then the default handling of arrays (which - //is generating an IN-statement) should apply - pValue = vars.get(pValue); - if (pMandatory && pValue === null) - throw SqlBuilder._ERROR_VALUE_IS_MANDATORY_JDITO_VAR(); - typeofValue = typeof pValue; - } - - // remove the first $ if there are two $ - if (typeofValue == "string" && pValue.length >= 2 && pValue[0] == "$" && pValue[1] == "$") - pValue = pValue.slice(1); - - //support for Set by converting to Array - if (pValue instanceof Set) - pValue = Array.from(pValue); - - // pValue can be... - // ... a SqlBuilder / Prepared statement array -> it is a SqlBuilder containing a complete subquery or an simple array (in statement) - if (pValue instanceof SqlBuilder || Array.isArray(pValue) || (typeofValue == "string" && (pFieldOrCond == undefined || pFieldOrCond == null))) - { - // check if the array is really a value-array for an in and not a prepared statement - if (Array.isArray(pValue) && (pValue.length <= 1 || !Array.isArray(pValue[1]))) - { - if (pValue.length == 0) - { - if (pMandatory) - throw SqlBuilder._ERROR_VALUE_IS_MANDATORY(); - - return this; - } - - // if it is null -> ignore it. -> the pCondition should not contain a # in this case - if (field != null) - { - var [alias, parsedField] = SqlUtils.parseField(field) - if (pFieldType === undefined || pFieldType === null) - pFieldType = SqlUtils.getSingleColumnType(parsedField, undefined, this.alias); - } - //overwrite condition to set a default behaviour - if (pCondition == undefined) - pCondition = SqlBuilder.IN(); - // value-array -> convert it to a prepared statement ["(?, ?, ?)", [[val1, type1], [val2, type2], [val3, type3]]] - this._whereCondition(this._prepare(field, SqlUtils.getSqlInStatement(undefined, pValue, undefined, true, pFieldType), pCondition, pFieldType, false), undefined, pAddPreparedConditionCallback, true); - return this; - } - - if (pFieldOrCond !== null && pFieldOrCond !== undefined) - { - if (!pCondition) - pCondition = SqlBuilder.EQUAL(); - - pCondition = SqlUtils.replaceConditionTemplate(pCondition, '#', SqlUtils.parseField(pFieldOrCond)[0]) - } - else - { - if (!pCondition) - pCondition = "?" - } - - // _whereSubquery can handle SqlBuilder and prepared statements as value - return this._whereSubquery(pValue, pMandatory, pCondition, pAddPreparedConditionCallback); - } - - if (!pCondition) - pCondition = SqlBuilder.EQUAL(); - - // ... everything else -> just pass it - if (pValue === false || pValue === 0 || pValue === "" || pValue) - { - let prep = this._prepare(field, pValue, pCondition, pFieldType) - this._whereCondition(prep, undefined, pAddPreparedConditionCallback); - } - - return this; - } - - - throw SqlBuilder._ERROR_UNSUPPORTED_PARAMETER_COMBINATION(); + this._groupBy = SqlBuilder._getStatement(pFields, "group by", undefined, true); + return this; } /** - * helper function to add a condition via "and" - * - * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} pFieldOrCond see .where() - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} pValue see .where() - * @param {Boolean} [pMandatory=true] if true: throw error if pValue is null, undefined, SqlBuilder without condition, etc... else just ignore the condition - * @param {String} [pCondition="# = ?"] see .where() - * @param {SQLTYPES|Numeric} [pFieldType=AutomaticallyLoadedType] see .where() + * Adds another SqlBuilder object or select string with union. * - * @ignore + * @param {SqlBuilder|String} pSelect + * @return {SqlBuilder} current SqlBuilder object */ -SqlBuilder.prototype._and = function(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType) +SqlBuilder.prototype.union = function(pSelect) { - 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])) - { - if (that.hasCondition()) - that._where.sqlStorage += " and "; - - that._where.sqlStorage += pPreparedCondition[0]; - that._where.preparedValues = that._where.preparedValues.concat(pPreparedCondition[1]); - } - }); + this._unions.push(SqlBuilder._getStatement(pSelect, "union")); + return this; } /** - * helper function to add a condition via "or" - * The callback inside of this function adds brackets where needed. - * - * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} pFieldOrCond see .where() - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} pValue see .where() - * @param {Boolean} [pMandatory=true] if true: throw error if pValue is null, undefined, SqlBuilder without condition, etc... else just ignore the condition - * @param {String} [pCondition="# = ?"] see .where() - * @param {SQLTYPES|Numeric} [pFieldType=AutomaticallyLoadedType] see .where() + * Adds another SqlBuilder object or select string with union all. * - * @ignore + * @param {SqlBuilder|String} pSelect + * @return {SqlBuilder} current SqlBuilder object */ -SqlBuilder.prototype._or = function(pFieldOrCond, pValue, pMandatory, pCondition, pFieldType) -{ - 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])) - { - if (that._where._previouslyOnlyOr) - { - that._where.sqlStorage = that._where.sqlStorage + " or " + pPreparedCondition[0]; - that._where._lastWasOr = true; - } - else if (that.hasCondition()) - { - let cond = pPreparedCondition[0]; - - if (!pAlreadySurroundedByBrackets) - cond = "(" + cond + ")"; - - if (that._where._lastWasOr) - that._where.sqlStorage = that._where.sqlStorage + " or " + cond; - else - that._where.sqlStorage = "(" + that._where.sqlStorage + ") or " + cond; - - that._where._lastWasOr = true; - } - else - { - if (!that.hasCondition()) - that._where._previouslyOnlyOr = true; - - that._where.sqlStorage = pPreparedCondition[0]; - } - that._where.preparedValues = that._where.preparedValues.concat(pPreparedCondition[1]); - } - }); +SqlBuilder.prototype.unionAll = function(pSelect) +{ + this._unions.push(SqlBuilder._getStatement(pSelect, "union all")); + return this; } /** - * Constant-like function which provides a value for pCondition if you need a "not" statement. - * - * @return {String} + * Adds a having clause to the sql. * - * @example - * var cond = newWhere(null, someCondition, SqlBuilder.NOT()) + * @param {String|SqlBuilder} [pCondition] The having condition. This can be + * - a string (without the where keyword) + * - a SqlBuilder NOTE: only the condition is used from it + * + * @return {SqlBuilder} current SqlBuilder object */ -SqlBuilder.NOT = function() +SqlBuilder.prototype.having = function(pCondition) { - return "not ?"; + this._having = SqlBuilder._getStatement(pCondition, "having"); + return this; } /** - * Constant-like function which provides a value for pCondition if you need a "not in" statement. - * - * @return {String} - * - * @example - * var cond = newWhere("PERSON.FIRSTNAME", ["Fritz"], SqlBuilder.NOT_IN()) + * checks if conditions have been added to the object + * @return {Boolean} true if conditions have been added, false when not */ -SqlBuilder.NOT_IN = function() -{ - return "# not in ?"; +SqlBuilder.prototype.hasCondition = function() { + if (this._where.sqlStorage) + return true; + return false; } /** - * Constant-like function which provides a value for pCondition if you need a "in" statement. - * - * @return {String} - * - * @example - * var cond = newWhere("PERSON.FIRSTNAME", ["Fritz"], SqlBuilder.IN()) + * checks if conditions have been added to the object + * Note: this does not nessecarily mean that hasCondition() is true + * @return {Boolean} true if .where was already called */ -SqlBuilder.IN = function() -{ - return "# in ?"; +SqlBuilder.prototype.whereWasCalled = function() { + return this._where._whereWasCalled; } /** - * Constant-like function which provides a value for pCondition if you need a "exists" statement. - * - * @return {String} - * - * @example - * var cond = newWhere(null, mySubSqlBuilder, SqlBuilder.EXISTS()) + * checks if all mandatory parts to execute the select have been added to the object + * ("select" and "from" parts) + * @return {Boolean} true if select and from have been added, false if not */ -SqlBuilder.EXISTS = function() +SqlBuilder.prototype.isFullSelect = function() { - return "exists ?"; + return !(!this._select || !this._from); } /** - * Constant-like function which provides a value for pCondition if you need a "not exists" statement. - * - * @return {String} + * Function that resets the current where-condition as if no conditions would have been added + * this is usefull if you want to reuse the same Builder over and over again with a different condition. + * This also resets whether where was already called, so you have to use .where to add a condition after this. * - * @example - * var cond = newWhere(null, mySubSqlBuilder, SqlBuilder.NOT_EXISTS()) + * @return {SqlBuilder} current SqlBuilder object */ -SqlBuilder.NOT_EXISTS = function() +SqlBuilder.prototype.clearWhere = function() { - return "not exists ?"; + this._initWhere(); + return this; } /** - * Constant-like function which provides a value for pCondition if you need a "year(#) = ?" statement. - * If you use this, the default pFieldType will be SQLTYPES.INTEGER. - * - * @return {Function} - * - * @example - * var cond = newWhere("FORECAST.DATE_START", DateUtils.getCurrentYear(), SqlBuilder.YEAR_EQUALS()); + * function that initializes the properties of the ._where object, this is used in the + * constructor and .clearWhere */ -SqlBuilder.YEAR_EQUALS = function () +SqlBuilder.prototype._initWhere = function () { - //function will be called later so it can use the alias of the SqlBuilder - return function (pAlias) {return [(new SqlMaskingUtils(pAlias).yearFromDate("#")) + " = ?", SQLTYPES.INTEGER];}; + //TODO: maybe put conditions in an object/array for better internal object structure + this._where.sqlStorage = ""; + this._where.preparedValues = []; + this._where._lastWasOr = false; // save, if the last condition was an OR. For better bracket-placement + this._where._previouslyOnlyOr = false; // also for better bracket-placement + this._where._whereWasCalled = false; // where has always to be called first for a better semantic } /** - * Constant-like function which provides a value for pCondition if you need a "# = ?" statement. - * This is the default for the pCondition parameter, so it can be omitted. + * helper function for composing preparedStatements <br/> + * <br/> + * see .where for more information about the parameters * - * @return {String} + * @param {String | String[]} pField the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] + * @param {String} pValue the value that shall be set into the prepared statement + * @param {String} pCondition the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; <br/> + * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" <br/> + * Default is "# = ?" + * @param {Numeric | Boolean} [pFieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; <br/> + * The loaded type is cached if no type is given. So it is also safe to use this in a loop. <br/> + * e.g. <br/> + * for (...) { <br/> + * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") <br/> + * } + * @param {Boolean} pSubselectBrackets if true, brackets are added to subselects + * @return {PreparedSqlArray} a preparedSqlArray built out of the given parameters * - * @example - * var cond = newWhere("PERSON.FIRSTNAME", "Fritz", SqlBuilder.EQUAL()) + * @ignore */ -SqlBuilder.EQUAL = function () +SqlBuilder.prototype._prepare = function(pField, pValue, pCondition, pFieldType, pSubselectBrackets) { - return "# = ?"; -} + if (pSubselectBrackets == undefined) + pSubselectBrackets = true; + + if (pValue == undefined) + throw new Error(translate.withArguments("${SQL_LIB_UNDEFINED_VALUE} field: %0", [pField])); + + if (pCondition == undefined) + pCondition = SqlBuilder.EQUAL(); -/** - * Constant-like function which provides a value for pCondition if you need a "# <> ?" statement. - * - * @return {String} - * - * @example - * var cond = newWhere("PERSON.FIRSTNAME", "Fritz", SqlBuilder.NOT_EQUALS()) - */ -SqlBuilder.NOT_EQUAL = function () -{ - return "# <> ?"; -} + var alias, field; + if (pField != null) + { + [alias, field] = SqlUtils.parseField(pField) + if (pFieldType == undefined) + pFieldType = SqlUtils.getSingleColumnType(pField, undefined, this.alias); + + var table = SqlUtils._parseFieldQualifier(pField).table; + //Set the table for update/delete if it isn't already set, so you don't need to specify the table if the where-condition contains it + if (table && !this._tableName) + this._tableName = table; + } + + var values = []; -/** - * Constant-like function which provides a value for pCondition if you need a "# like ?" statement. - * - * @return {String} - * - * @example - * var cond = newWhere("PERSON.FIRSTNAME", "F%", SqlBuilder.LIKE()) - */ -SqlBuilder.LIKE = function () -{ - return "# like ?"; -} + // If subselect: replace '?' with the subselect + if (Array.isArray(pValue)) + { + pCondition = SqlUtils.replaceConditionTemplate(pCondition, "\\?", (pSubselectBrackets ? " ( " : " ") + pValue[0] + (pSubselectBrackets ? " ) " : " ")); + values = pValue[1]; + } + else + { + var type = pFieldType; + //booleans are normally stored with TINYINT, this is to support true/false directly + if (_isIntegerType(type) && Utils.isBoolean(pValue)) + pValue = Number(pValue); + values = [[pValue.toString(), type]]; + } -/** - * Constant-like function which provides a value for pCondition if you need a "# like ?" statement. - * - * @return {String} - * - * @example - * var cond = newWhere("PERSON.FIRSTNAME", "F%", SqlBuilder.NOT_LIKE()) - */ -SqlBuilder.NOT_LIKE = function () -{ - return "# not like ?"; + if (pField != null) + pCondition = SqlUtils.replaceConditionTemplate(pCondition, "#", alias); + + return [pCondition, values]; + + function _isIntegerType (pType) + { + return pType == SQLTYPES.TINYINT + || pType == SQLTYPES.SMALLINT + || pType == SQLTYPES.INTEGER + || pType == SQLTYPES.BIGINT; + } } /** - * Constant-like function which provides a value for pCondition if you need a "# > ?" statement. + * generates a part of the sql * - * @return {String} - */ -SqlBuilder.GREATER = function () -{ - return "# > ?"; -} - -/** - * Constant-like function which provides a value for pCondition if you need a "# < ?" statement. + * @param {String|String[]|SqlBuilder} pElement the element to append + * @param {String} [pPrefix] string to be added before pElement + * @param {String} [pPostfix] string to be added after pElement + * @param {Boolean} [pAutoJoin=false] if this is true and pElement is an array, it will be automatically <br/> + * joined together to a string + * @param {Boolean} [pUseSubselectAlias=false] if true the subselectAlias is added if the element is a subquery * - * @return {String} + * @ignore */ -SqlBuilder.LESS = function () +SqlBuilder._getStatement = function (pElement, pPrefix, pPostfix, pAutoJoin, pUseSubselectAlias) { - return "# < ?"; -} + var preparedValues = []; + if (typeof pElement !== "string") + { + if (Array.isArray(pElement) && pElement.length !== undefined && pAutoJoin) //array of fields + { + for (let i = 0; i < pElement.length; i++) + { + if (typeof pElement[i] !== "string") + pElement[i] = _getElement(pElement[i]); + } + + pElement = ArrayUtils.joinNonEmptyFields(pElement, ", "); + } + else + { + pElement = _getElement(pElement); + } + } -/** - * Constant-like function which provides a value for pCondition if you need a "# >= ?" statement. - * - * @return {String} - */ -SqlBuilder.GREATER_OR_EQUAL = function () -{ - return "# >= ?"; + if (pPrefix && pElement) + pElement = pPrefix + " " + pElement; + if (pPostfix && pElement) + pElement += " " + pPostfix; + + return { + preparedValues: preparedValues, + sqlStorage: pElement.toString() + }; + + function _getElement (element) + { + var isSubQuery = false; + var subselectAlias = ""; + if (SqlBuilder.checkCanBuildSql(element)) + { + if (element instanceof SqlBuilder && element.isFullSelect()) + { + isSubQuery = true; + + if (pUseSubselectAlias && element._subselectAlias) + subselectAlias = " " + element._subselectAlias; + } + element = element.build(); + } + preparedValues = preparedValues.concat(element[1]); + if (isSubQuery || pAutoJoin) + return "(" + element[0] + ")" + subselectAlias; + return element[0]; + } } /** - * Constant-like function which provides a value for pCondition if you need a "# <= ?" statement. + * builds a prepared condition out of the object. Only the condition is used. Select, from, ... are ignored. * - * @return {String} + * @return {PreparedSqlArray} prepared condition */ -SqlBuilder.LESS_OR_EQUAL = function () -{ - return "# <= ?"; +SqlBuilder.prototype.buildCondition = function() +{ + return [this._where.sqlStorage, this._where.preparedValues]; } /** - * Constant-like function which returns an impossible condition ("1 = 2"). + * builds a prepared statement out of the object. If a part doesn't exit, it's just ignored. * - * @return {String} + * @param {String} [pDefaultConditionIfNone=""] a default condition string which should be used if the SqlBuilder doesn't have any condition + * @return {PreparedSqlArray} prepared statement */ -SqlBuilder.NORESULT_CONDITION = function () +SqlBuilder.prototype.build = function(pDefaultConditionIfNone) { - return "1 = 2"; -} - -/** - * Object providing constant-like functions for sql-any-conditions. - */ -SqlBuilder.ANY = { - /** - * Constant-like function that returns a "# = any ?" statement. - */ - EQUAL : function () {return "# = any ?";}, - /** - * Constant-like function that returns a "# <> any ?" statement. - */ - NOT_EQUAL : function () {return "# <> any ?";}, - /** - * Constant-like function that returns a "# > any ?" statement. - */ - GREATER : function () {return "# > any ?";}, - /** - * Constant-like function that returns a "# >= any ?" statement. - */ - GREATER_OR_EQUAL : function () {return "# >= any ?";}, - /** - * Constant-like function that returns a "# < any ?" statement. - */ - LESS : function () {return "# < any ?";}, - /** - * Constant-like function that returns a "# <= any ?" statement. - */ - LESS_OR_EQUAL : function () {return "# <= any ?";} -} - -/** - * Object providing constant-like functions for sql-all-conditions. - */ -SqlBuilder.ALL = { - /** - * Constant-like function that returns a "# = all ?" statement. - */ - EQUAL : function () {return "# = all ?";}, - /** - * Constant-like function that returns a "# <> all ?" statement. - */ - NOT_EQUAL : function () {return "# <> all ?";}, - /** - * Constant-like function that returns a "# > all ?" statement. - */ - GREATER : function () {return "# > all ?";}, - /** - * Constant-like function that returns a "# >= all ?" statement. - */ - GREATER_OR_EQUAL : function () {return "# >= all ?";}, - /** - * Constant-like function that returns a "# < all ?" statement. - */ - LESS : function () {return "# < all ?";}, - /** - * Constant-like function that returns a "# <= all ?" statement. - */ - LESS_OR_EQUAL : function () {return "# <= all ?";} + var wherePrefix = ""; + var fromObj = this._from; + + if (this.isFullSelect()) + { + fromObj = { + sqlStorage: "from " + this._from.sqlStorage, + preparedValues: this._from.preparedValues + }; + if (this._where.sqlStorage) + wherePrefix = "where "; + } + + var whereSql = this._where.sqlStorage; + + if (!this.hasCondition() && pDefaultConditionIfNone) + whereSql = wherePrefix + pDefaultConditionIfNone; + + var whereObj = { + sqlStorage : wherePrefix + whereSql, + preparedValues : this._where.preparedValues + }; + + var allParts = [ + this._select, + fromObj + ].concat(this._joins).concat([ + whereObj, + this._groupBy, + this._having, + this._orderBy + ]).concat(this._unions); + + var sqlStr = ""; + var preparedVals = []; + for (let i = 0, l = allParts.length; i < l; i++) + { + let part = allParts[i]; + if (part) + { + if (sqlStr && part.sqlStorage) + sqlStr += " "; + sqlStr += part.sqlStorage; + if (part.preparedValues.length) + preparedVals = preparedVals.concat(part.preparedValues); + } + } + + return [sqlStr, preparedVals]; } /** - * Throws an error if pValue is null, undefined or a SqlBuilder without condition (or if pValue is a $-variable: error if the result of it is null or undefined)<br/> - * Also throws an error if pFieldOrCond is the only parameter and it is null<br/> - * <br/> - * Adds a condition by using "or" to the Sql.<br/> - * Note: You have to call .where before using .and / .or (this is mainly for semantic reasons) + * Updates data in the database.<br/> + * Note: the default for pExecuteOnlyIfConditionExists is true to prevent updating all rows if the SqlBuilder has no condition. * - * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] If this is the only parameter, it is used as Condition <br/> - * else it is used as Field.<br/> - * <br/> - * If you use it as Subselect (the only param), it can be:<br/> - * - a string: just added as it is<br/> - * - a PreparedSqlArray: an Array in this form: [sqlStr, [[value1, type1], [valueN, typeN]]]<br/> - * the sql is just used as it is.<br/> - * - a SqlBuilder: ONLY THE CONDITION is used from it<br/> - * <br/> - * If you use it as a Field (at least pValue has to be filled), this param provides the field information to<br/> - * load the SQLTYPE for this condition. <br/> - * It can be provided in the following ways:<br/> - * - a string: ONLY in this form: "TABLENAME.COLUMNNAME" <br/> - * Note1: you may have problems with names containing a '.' Use the next variant (as array) in this case<br/> - * Note2: if you need a table alias use the next variant (as array)<br/> - * - a array: ["TABLENAME", "COLUMNNAME", "tableAlias"] OR ["TABLENAME", "COLUMNNAME"]<br/> - * Note: this can also be null if you don't need the field and use a pCondition without a # - * - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value which is used for the condition.<br/> - * Basically it can be nearly everything you need.<br/> - * - String, etc: is just used as value for the prepared statement. Of course it has to fit the type of the db-column<br/> - * - String starting with '$' is treated as jdito-variable: is loaded with vars.getString("$..."). <br/> - * Note: Use 2 '$' to escape the $ if you don't want it to be treated as JditoVar - * - * @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 - * - * @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/> - * then the db-field is DATETIME, but the value is INTEGER. In this case you can overwrite the type. - * - * @return {SqlBuilder} current SqlBuilder object + * @param {Boolean} [pExecuteOnlyIfConditionExists=true] If true, the update is only done if there is a condition.<br/> + * <strong>IMPORTANT: If this is set to false and there is no condition, every row in the table will be updated!</strong> + * @param {String} [pTableName] The table for updating data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, + * the table of the first where-condition is used. + * @param {String[]} pColumns The columns where you want to update. + * @param {SQLTYPES[]} [pColumnTypes=null] normally you can set this to null as the types are calculated if not provided + * @param {String[]} pValues The values to be updated. + * @param {Number} [pTimeout=-1] + * @return {Number} the number of rows affected + * @throws {Error} if no table is defined */ -SqlBuilder.prototype.or = function(pFieldOrCond, pValue, pCondition, pFieldType) +SqlBuilder.prototype.updateData = function(pExecuteOnlyIfConditionExists, pTableName, pColumns, pColumnTypes, pValues, pTimeout) { - return this._or(pFieldOrCond, pValue, true, pCondition, pFieldType); + if (this._checkForUpdate(pExecuteOnlyIfConditionExists)) + { + if (!pTableName && !this._tableName) + throw SqlBuilder._ERROR_NO_TABLE(); + + if (!pColumns) + pColumns = null; + + return db.updateData( + (pTableName ? pTableName : this._tableName), + pColumns, + pColumnTypes, + pValues, + this.buildCondition(), + (this.alias ? this.alias : db.getCurrentAlias()), + (pTimeout ? pTimeout : -1)); + } + + return 0; } /** - * Difference to or(): or() throws errors on invalid values, orIfSet just ignores the condition and does nothing (usefull e.g. for the parameter variables ("$param.ddd") in conditionProcesses.)<br/> - * <br/> - * Adds a condition by using "or" to the Sql.<br/> - * Note: You have to call .where before using .and / .or (this is mainly for semantic reasons) + * Updates data in the database. This function calls SqlBuilder.prototype.updateData, but provides a shorter syntax to + * improve the readability. * - * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] If this is the only parameter, it is used as Condition <br/> - * else it is used as Field.<br/> - * <br/> - * If you use it as Subselect (the only param), it can be:<br/> - * - a string: just added as it is<br/> - * - a PreparedSqlArray: an Array in this form: [sqlStr, [[value1, type1], [valueN, typeN]]]<br/> - * the sql is just used as it is.<br/> - * - a SqlBuilder: ONLY THE CONDITION is used from it<br/> - * <br/> - * If you use it as a Field (at least pValue has to be filled), this param provides the field information to<br/> - * load the SQLTYPE for this condition. <br/> - * It can be provided in the following ways:<br/> - * - a string: ONLY in this form: "TABLENAME.COLUMNNAME" <br/> - * Note1: you may have problems with names containing a '.' Use the next variant (as array) in this case<br/> - * Note2: if you need a table alias use the next variant (as array)<br/> - * - a array: ["TABLENAME", "COLUMNNAME", "tableAlias"] OR ["TABLENAME", "COLUMNNAME"]<br/> - * Note: this can also be null if you don't need the field and use a pCondition without a # - * - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value which is used for the condition.<br/> - * Basically it can be nearly everything you need.<br/> - * - String, etc: is just used as value for the prepared statement. Of course it has to fit the type of the db-column<br/> - * - String starting with '$' is treated as jdito-variable: is loaded with vars.getString("$..."). <br/> - * Note: Use 2 '$' to escape the $ if you don't want it to be treated as JditoVar - * - * @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 - * - * @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/> - * then the db-field is DATETIME, but the value is INTEGER. In this case you can overwrite the type. - * - * @return {SqlBuilder} current SqlBuilder object + * @param {Object|Map} pFieldValues Object with the columns to update as keys mapped to their values + * @param {String} [pTableName] The table for updating data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, + * the table of the first where-condition is used. + * @return {Number} the number of rows affected + * @example + * newWhere("SALESORDER.SALESORDERID", "$field.SALESORDERID") + * .updateFields({"ORDERSTATUS" : "1"}); //pTableName can be omitted here since it's clearly defined by the given condition */ -SqlBuilder.prototype.orIfSet = function(pFieldOrCond, pValue, pCondition, pFieldType) +SqlBuilder.prototype.updateFields = function (pFieldValues, pTableName) { - return this._or(pFieldOrCond, pValue, false, pCondition, pFieldType); + if (!pFieldValues || typeof(pFieldValues) !== "object") + throw SqlBuilder._ERROR_UPDATE_VALUES_INVALID; + + var columnValues = SqlBuilder._columnsValuesFromObject(pFieldValues, true); + if (columnValues.columns.length === 0) + return 0; + return this.updateData(true, pTableName, columnValues.columns, null, columnValues.values); } /** - * Throws an error if pValue is null, undefined or a SqlBuilder without condition (or if pValue is a $-variable: error if the result of it is null or undefined)<br/> - * Also throws an error if pFieldOrCond is the only parameter and it is null<br/> - * <br/> - * Adds a condition by using "and" to the Sql.<br/> - * Note: You have to call .where before using .and / .or (this is mainly for semantic reasons) + * Builds an array containing the table and condition for an update. * - * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] If this is the only parameter, it is used as Condition <br/> - * else it is used as Field.<br/> - * <br/> - * If you use it as Subselect (the only param), it can be:<br/> - * - a string: just added as it is<br/> - * - a PreparedSqlArray: an Array in this form: [sqlStr, [[value1, type1], [valueN, typeN]]]<br/> - * the sql is just used as it is.<br/> - * - a SqlBuilder: ONLY THE CONDITION is used from it<br/> - * <br/> - * If you use it as a Field (at least pValue has to be filled), this param provides the field information to<br/> - * load the SQLTYPE for this condition. <br/> - * It can be provided in the following ways:<br/> - * - a string: ONLY in this form: "TABLENAME.COLUMNNAME" <br/> - * Note1: you may have problems with names containing a '.' Use the next variant (as array) in this case<br/> - * Note2: if you need a table alias use the next variant (as array)<br/> - * - a array: ["TABLENAME", "COLUMNNAME", "tableAlias"] OR ["TABLENAME", "COLUMNNAME"]<br/> - * Note: this can also be null if you don't need the field and use a pCondition without a # - * - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value which is used for the condition.<br/> - * Basically it can be nearly everything you need.<br/> - * - String, etc: is just used as value for the prepared statement. Of course it has to fit the type of the db-column<br/> - * - String starting with '$' is treated as jdito-variable: is loaded with vars.getString("$..."). <br/> - * Note: Use 2 '$' to escape the $ if you don't want it to be treated as JditoVar - * - * @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 - * - * @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/> - * then the db-field is DATETIME, but the value is INTEGER. In this case you can overwrite the type. - * - * @return {SqlBuilder} current SqlBuilder object + * @param {Object|Map} pFieldValues Object with the columns to update as keys mapped to their values + * @param {String} [pTableName] The table for updating data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, + * the table of the first where-condition is used. + * @return {Array} array of [tableName, columns, columnTypes, values, preparedCondition], like it is required by db.updates or null if there is no condition + * @example + * var updateStatements = []; + * updateStatements.push(newWhere("PERSON.PERSONID", pPersonId).buildUpdateStatement({"FIRSTNAME" : firstName})); + * updateStatements.push(newWhere("ORGANISATION.ORGANISATIONID", pOrganisationId).buildUpdateStatement({"NAME" : organisationName})); + * db.updates(updateStatements); */ -SqlBuilder.prototype.and = function(pFieldOrCond, pValue, pCondition, pFieldType) +SqlBuilder.prototype.buildUpdateStatement = function (pFieldValues, pTableName) { - return this._and(pFieldOrCond, pValue, true, pCondition, pFieldType); + if (!pFieldValues || typeof(pFieldValues) !== "object") + throw SqlBuilder._ERROR_UPDATE_VALUES_INVALID; + + var columnValues = SqlBuilder._columnsValuesFromObject(pFieldValues, true); + if (columnValues.columns.length !== 0 && this._checkForUpdate()) + { + if (!pTableName && !this._tableName) + throw SqlBuilder._ERROR_NO_TABLE(); + + return [ + (pTableName ? pTableName : this._tableName), + columnValues.columns, + null, + columnValues.values, + this.buildCondition() + ]; + } + return null; } /** - * Difference to and(): and() throws errors on invalid values, andIfSet just ignores the condition and does nothing (usefull e.g. for the parameter variables ("$param.ddd") in conditionProcesses.)<br/> - * <br/> - * Adds a condition by using "and" to the Sql.<br/> - * Note: You have to call .where before using .and / .or (this is mainly for semantic reasons) + * Builds an array containing the data for an insert. * - * @param {String|String[]|SqlBuilder|PreparedSqlArray|null} [pFieldOrCond] If this is the only parameter, it is used as Condition <br/> - * else it is used as Field.<br/> - * <br/> - * If you use it as Subselect (the only param), it can be:<br/> - * - a string: just added as it is<br/> - * - a PreparedSqlArray: an Array in this form: [sqlStr, [[value1, type1], [valueN, typeN]]]<br/> - * the sql is just used as it is.<br/> - * - a SqlBuilder: ONLY THE CONDITION is used from it<br/> - * <br/> - * If you use it as a Field (at least pValue has to be filled), this param provides the field information to<br/> - * load the SQLTYPE for this condition. <br/> - * It can be provided in the following ways:<br/> - * - a string: ONLY in this form: "TABLENAME.COLUMNNAME" <br/> - * Note1: you may have problems with names containing a '.' Use the next variant (as array) in this case<br/> - * Note2: if you need a table alias use the next variant (as array)<br/> - * - a array: ["TABLENAME", "COLUMNNAME", "tableAlias"] OR ["TABLENAME", "COLUMNNAME"]<br/> - * Note: this can also be null if you don't need the field and use a pCondition without a # - * - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value which is used for the condition.<br/> - * Basically it can be nearly everything you need.<br/> - * - String, etc: is just used as value for the prepared statement. Of course it has to fit the type of the db-column<br/> - * - String starting with '$' is treated as jdito-variable: is loaded with vars.getString("$..."). <br/> - * Note: Use 2 '$' to escape the $ if you don't want it to be treated as JditoVar - * - * @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 - * - * @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/> - * then the db-field is DATETIME, but the value is INTEGER. In this case you can overwrite the type. - * - * @return {SqlBuilder} current SqlBuilder object + * @param {Object|Map} pFieldValues Object with the columns to insert into as keys mapped to their values + * @param {String} [pTableName] The table for updating data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, + * the table of the first where-condition is used. + * @param {String} [pAutoUidField] UID column that should be filled with a random UUID + * @return {Array} array of [tableName, columns, columnTypes, values, preparedCondition], like it is required by db.updates or null if there is no condition */ -SqlBuilder.prototype.andIfSet = function(pFieldOrCond, pValue, pCondition, pFieldType) +SqlBuilder.prototype.buildInsertStatement = function (pFieldValues, pTableName, pAutoUidField) { - return this._and(pFieldOrCond, pValue, false, pCondition, pFieldType); + if (!pFieldValues || !Utils.isObject(pFieldValues)) + throw SqlBuilder._ERROR_UPDATE_VALUES_INVALID; + + if (pAutoUidField) + pFieldValues[pAutoUidField] = util.getNewUUID(); + + var columnValues = SqlBuilder._columnsValuesFromObject(pFieldValues); + if (columnValues.columns.length !== 0) + { + if (!pTableName && !this._tableName) + throw SqlBuilder._ERROR_NO_TABLE(); + + return [ + (pTableName ? pTableName : this._tableName), + columnValues.columns, + null, + columnValues.values + ]; + } + return null; } /** - * Sets the order by clause of the sql. + * Inserts data in the database. This function doesn't require any where-condition, it is intended to be called right after 'new SqlBuilder()'. <br/> * - * @param {String|String[]} pOrderBy a string is added as it is, a array is concatenated by ', ' - * @return {SqlBuilder} current SqlBuilder object + * @param {String} [pTableName] The table for inserting data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, + * the table of the first where-condition is used. + * @param {String[]} pColumns The columns where you want to insert into. + * @param {SQLTYPES[]} [pColumnTypes=null] normally you can set this to null as the types are calculated if not provided + * @param {String[]} pValues The values to be inserted. + * @param {Number} [pTimeout=-1] + * @return {Number} the number of rows affected + * @throws {Error} if no table is defined */ -SqlBuilder.prototype.orderBy = function(pOrderBy) +SqlBuilder.prototype.insertData = function(pTableName, pColumns, pColumnTypes, pValues, pTimeout) { - this._orderBy = SqlBuilder._getStatement(pOrderBy, "order by", undefined, true); - return this; + if (!pTableName && !this._tableName) + throw SqlBuilder._ERROR_NO_TABLE(); + + if (!pColumns) + pColumns = null; + + return db.insertData( + (pTableName ? pTableName : this._tableName), + pColumns, + pColumnTypes, + pValues, + (this.alias ? this.alias : db.getCurrentAlias()), + (pTimeout ? pTimeout : -1)); } /** - * Sets the group by clause of the sql. + * Inserts data in the database. This function calls SqlBuilder.prototype.insertData, but provides a shorter syntax to + * improve the readability. * - * @param {String|String[]} pFields a string is added as it is, a array is concatenated by ', ' - * @return {SqlBuilder} current SqlBuilder object + * @param {Object|Map} pFieldValues Object with the columns to update as keys mapped to their values + * @param {String} [pTableName] The table for updating data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, + * the table of the first where-condition is used. + * @param {String} [pAutoUidField] UID column that should be filled with a random UUID + * @return {Number} the number of rows affected + * @example + * new SqlBuilder().insertFields({ + * "ACTIVITY_ID" : pActivityId, + * "OBJECT_ROWID" : pRowId, + * "OBJECT_TYPE" : pObjectType + * }, "ACTIVITYLINK", "ACTIVITYLINKID"); */ -SqlBuilder.prototype.groupBy = function(pFields) +SqlBuilder.prototype.insertFields = function (pFieldValues, pTableName, pAutoUidField) { - this._groupBy = SqlBuilder._getStatement(pFields, "group by", undefined, true); - return this; + if (!pFieldValues || typeof(pFieldValues) !== "object") + throw SqlBuilder._ERROR_UPDATE_VALUES_INVALID; + + if (pAutoUidField) + pFieldValues[pAutoUidField] = util.getNewUUID(); + + var columnValues = SqlBuilder._columnsValuesFromObject(pFieldValues); + if (columnValues.columns.length === 0) + return 0; + return this.insertData(pTableName, columnValues.columns, null, columnValues.values); } -/** - * Adds another SqlBuilder object or select string with union. - * - * @param {SqlBuilder|String} pSelect - * @return {SqlBuilder} current SqlBuilder object - */ -SqlBuilder.prototype.union = function(pSelect) +SqlBuilder._columnsValuesFromObject = function (pFieldValues, pIncludeNullValues) { - this._unions.push(SqlBuilder._getStatement(pSelect, "union")); - return this; + var columns = []; + var values = []; + if (Utils.isMap(pFieldValues)) + { + pFieldValues.forEach(function (value, key) + { + if (pIncludeNullValues || (value !== undefined && value !== null)) + { + columns.push(key); + values.push(_valueToString(value)); + } + }); + } + else + { + for (let field in pFieldValues) + { + if (pIncludeNullValues || (pFieldValues[field] !== undefined && pFieldValues[field] !== null)) + { + columns.push(field); + values.push(_valueToString(pFieldValues[field])); + } + } + } + return { + columns: columns, + values: values + }; + + function _valueToString (pValue) + { + if (pValue === undefined || pValue === null) + return ""; + return pValue.toString(); + } } /** - * Adds another SqlBuilder object or select string with union all. - * - * @param {SqlBuilder|String} pSelect - * @return {SqlBuilder} current SqlBuilder object + * Deletes data from the database.<br/> + * Note: the default for pExecuteOnlyIfConditionExists is true to prevent updating all rows if the SqlBuilder has no condition. + + * @param {Boolean} [pExecuteOnlyIfConditionExists=true] If true, the deletion is only done if there is a condition.<br/> + * <strong>IMPORTANT: If this is set to false and there is no condition, every row in the table will be deleted!</strong> + * @param {String} [pTableName] The table for deleting data. If undefined, the from part of the SqlBuilder will be used. If no from is set, + * the table of the first where-condition is used. + * @param {Number} [pTimeout=-1] + * @return {Number} the number of rows affected + * @throws {Error} if no table is defined + * @example + * newWhere("AB_ATTRIBUTE.AB_ATTRIBUTEID", "$local.uid") + * .deleteData(); //pTableName can be omitted here since it's clearly defined by the given condition */ -SqlBuilder.prototype.unionAll = function(pSelect) +SqlBuilder.prototype.deleteData = function(pExecuteOnlyIfConditionExists, pTableName, pTimeout) { - this._unions.push(SqlBuilder._getStatement(pSelect, "union all")); - return this; + if (this._checkForUpdate(pExecuteOnlyIfConditionExists)) + { + if (!pTableName && !this._tableName) + throw SqlBuilder._ERROR_NO_TABLE(); + + return db.deleteData( + (pTableName ? pTableName : this._tableName), + this.buildCondition(), + (this.alias ? this.alias : db.getCurrentAlias()), + (pTimeout ? pTimeout : -1)); + } + else + { + return 0; + } } /** - * Adds a having clause to the sql. + * Builds an array containing the table and condition for a delete. * - * @param {String|SqlBuilder} [pCondition] The having condition. This can be - * - a string (without the where keyword) - * - a SqlBuilder NOTE: only the condition is used from it - * - * @return {SqlBuilder} current SqlBuilder object + * @param {Boolean} [pOnlyIfConditionExists=true] If true and there is no condition, null is returned.<br/> + * <strong>IMPORTANT: If this is set to false and there is no condition, every row in the table will be deleted!</strong> + * @param {String} [pTableName] The table for deleting data. If undefined, the from part of the SqlBuilder will be used. If no from is set, + * the table of the first where-condition is used. + * @return {Array} array of [tableName, preparedCondition], like it is required by db.deletes + * @example + * var deleteStatements = []; + * deleteStatements.push(newWhere("PERSON.PERSONID", pPersonId).buildDeleteStatement()); + * deleteStatements.push(newWhere("CONTACT.CONTACTID", pContactId).buildDeleteStatement()); + * db.deletes(deleteStatements); */ -SqlBuilder.prototype.having = function(pCondition) +SqlBuilder.prototype.buildDeleteStatement = function(pOnlyIfConditionExists, pTableName) { - this._having = SqlBuilder._getStatement(pCondition, "having"); - return this; + if (this._checkForUpdate(pOnlyIfConditionExists)) + { + if (!pTableName && !this._tableName) + throw SqlBuilder._ERROR_NO_TABLE(); + + return [ + (pTableName ? pTableName : this._tableName), + this.buildCondition() + ]; + } + else + return null; } /** - * checks if conditions have been added to the object - * @return {Boolean} true if conditions have been added, false when not + * Executes the SQL using db.cell and returns the result.<br/> + * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. + * + * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, "" or the provided FallbackValue is returned + * @param {AnyValue} [pFallbackValue=""] here you can provide a fallback value if pExecuteOnlyIfConditionExists is true and the SqlBuilder has no condition.<br/> + * This is intended for e.g. select count(*) from ... because there a default value of "0" is more helpful + * @return {String} the result of the query */ -SqlBuilder.prototype.hasCondition = function() { - if (this._where.sqlStorage) - return true; - return false; +SqlBuilder.prototype.cell = function(pExecuteOnlyIfConditionExists, pFallbackValue) +{ + if (this._checkForSelect(pExecuteOnlyIfConditionExists)) + { + return db.cell(this.build(), + (this.alias ? this.alias : db.getCurrentAlias())); + } + else + { + return (pFallbackValue ? pFallbackValue : ""); + } } /** - * checks if conditions have been added to the object - * Note: this does not nessecarily mean that hasCondition() is true - * @return {Boolean} true if .where was already called + * Executes the SQL using db.array(db.ROW, ...) and returns the result.<br/> + * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. + * + * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned + * @param {Number} [pMaxRows=0] + * @param {Number} [pTimeout=-1] + * @return {String[]} the result of the query */ -SqlBuilder.prototype.whereWasCalled = function() { - return this._where._whereWasCalled; +SqlBuilder.prototype.arrayRow = function (pExecuteOnlyIfConditionExists, pMaxRows, pTimeout) +{ + return this.array(db.ROW, pExecuteOnlyIfConditionExists, pMaxRows, pTimeout); } /** - * checks if all mandatory parts to execute the select have been added to the object - * ("select" and "from" parts) - * @return {Boolean} true if select and from have been added, false if not + * Executes the SQL using db.array(db.COLUMN, ...) and returns the result.<br/> + * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. + * + * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned + * @param {Number} [pMaxRows=0] + * @param {Number} [pTimeout=-1] + * @return {String[]} the result of the query */ -SqlBuilder.prototype.isFullSelect = function() +SqlBuilder.prototype.arrayColumn = function (pExecuteOnlyIfConditionExists, pMaxRows, pTimeout) { - return !(!this._select || !this._from); + return this.array(db.COLUMN, pExecuteOnlyIfConditionExists, pMaxRows, pTimeout); } /** - * Function that resets the current where-condition as if no conditions would have been added - * this is usefull if you want to reuse the same Builder over and over again with a different condition. - * This also resets whether where was already called, so you have to use .where to add a condition after this. + * Executes the SQL using db.array and returns the result.<br/> + * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. * - * @return {SqlBuilder} current SqlBuilder object + * @param {Number} pType db.ROW or db.COLUMN + * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned + * @param {Number} [pMaxRows=0] + * @param {Number} [pTimeout=-1] + * @return {String[]} the result of the query */ -SqlBuilder.prototype.clearWhere = function() +SqlBuilder.prototype.array = function(pType, pExecuteOnlyIfConditionExists, pMaxRows, pTimeout) { - this._initWhere(); - return this; + if (this._checkForSelect(pExecuteOnlyIfConditionExists)) + { + return db.array(pType, this.build(), + (this.alias ? this.alias : db.getCurrentAlias()), + (pMaxRows ? pMaxRows : 0), + (pTimeout ? pTimeout : -1)); + } + else + { + return []; + } } /** - * function that initializes the properties of the ._where object, this is used in the - * constructor and .clearWhere + * Executes the SQL using db.arrayPage and returns the result.<br/> + * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. + * + * @param {Number} pType db.ROW or db.COLUMN + * @param {Number} pStartIndex + * @param {Number} pRowCount + * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned + * @param {Number} [pTimeout=-1] + * @return {String[]} the result of the query */ -SqlBuilder.prototype._initWhere = function () +SqlBuilder.prototype.arrayPage = function(pType, pStartIndex, pRowCount, pExecuteOnlyIfConditionExists, pTimeout) { - //TODO: maybe put conditions in an object/array for better internal object structure - this._where.sqlStorage = ""; - this._where.preparedValues = []; - this._where._lastWasOr = false; // save, if the last condition was an OR. For better bracket-placement - this._where._previouslyOnlyOr = false; // also for better bracket-placement - this._where._whereWasCalled = false; // where has always to be called first for a better semantic + if (this._checkForSelect(pExecuteOnlyIfConditionExists)) + { + return db.arrayPage(pType, this.build(), + (this.alias ? this.alias : db.getCurrentAlias()), + pStartIndex === undefined ? this._startRow : pStartIndex, + pRowCount === undefined ? this._pageSize : pRowCount, + (pTimeout ? pTimeout : -1)); + } + else + { + return []; + } } /** - * helper function for composing preparedStatements <br/> - * <br/> - * see .where for more information about the parameters - * - * @param {String | String[]} pField the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] - * @param {String} pValue the value that shall be set into the prepared statement - * @param {String} pCondition the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; <br/> - * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" <br/> - * Default is "# = ?" - * @param {Numeric | Boolean} [pFieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; <br/> - * The loaded type is cached if no type is given. So it is also safe to use this in a loop. <br/> - * e.g. <br/> - * for (...) { <br/> - * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") <br/> - * } - * @param {Boolean} pSubselectBrackets if true, brackets are added to subselects - * @return {PreparedSqlArray} a preparedSqlArray built out of the given parameters + * Executes the SQL using db.table and returns the result.<br/> + * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. * - * @ignore + * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned + * @param {Number} [pMaxRows=0] + * @param {Number} [pTimeout=-1] + * @return {String[][]} the result of the query */ -SqlBuilder.prototype._prepare = function(pField, pValue, pCondition, pFieldType, pSubselectBrackets) +SqlBuilder.prototype.table = function(pExecuteOnlyIfConditionExists, pMaxRows, pTimeout) { - if (pSubselectBrackets == undefined) - pSubselectBrackets = true; - - if (pValue == undefined) - throw new Error(translate.withArguments("${SQL_LIB_UNDEFINED_VALUE} field: %0", [pField])); - - if (pCondition == undefined) - pCondition = SqlBuilder.EQUAL(); - - var alias, field; - if (pField != null) + if (this._checkForSelect(pExecuteOnlyIfConditionExists)) { - [alias, field] = SqlUtils.parseField(pField) - if (pFieldType == undefined) - pFieldType = SqlUtils.getSingleColumnType(pField, undefined, this.alias); - - var table = SqlUtils._parseFieldQualifier(pField).table; - //Set the table for update/delete if it isn't already set, so you don't need to specify the table if the where-condition contains it - if (table && !this._tableName) - this._tableName = table; + return db.table(this.build(), + (this.alias ? this.alias : db.getCurrentAlias()), + (pMaxRows ? pMaxRows : 0), + (pTimeout ? pTimeout : -1)); } - - var values = []; - - // If subselect: replace '?' with the subselect - if (Array.isArray(pValue)) - { - pCondition = SqlUtils.replaceConditionTemplate(pCondition, "\\?", (pSubselectBrackets ? " ( " : " ") + pValue[0] + (pSubselectBrackets ? " ) " : " ")); - values = pValue[1]; - } else { - var type = pFieldType; - //booleans are normally stored with TINYINT, this is to support true/false directly - if (_isIntegerType(type) && Utils.isBoolean(pValue)) - pValue = Number(pValue); - values = [[pValue.toString(), type]]; + return []; } +} - if (pField != null) - pCondition = SqlUtils.replaceConditionTemplate(pCondition, "#", alias); - - return [pCondition, values]; - - function _isIntegerType (pType) +/** + * Executes the SQL using db.tablePage and returns the result.<br/> + * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. + * + * @param {Number} pStartIndex + * @param {Number} pRowCount + * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned + * @param {Number} [pTimeout=-1] + * @return {String[][]} the result of the query + */ +SqlBuilder.prototype.tablePage = function(pStartIndex, pRowCount, pExecuteOnlyIfConditionExists, pTimeout) +{ + if (this._checkForSelect(pExecuteOnlyIfConditionExists)) { - return pType == SQLTYPES.TINYINT - || pType == SQLTYPES.SMALLINT - || pType == SQLTYPES.INTEGER - || pType == SQLTYPES.BIGINT; + return db.tablePage(this.build(), + (this.alias ? this.alias : db.getCurrentAlias()), + pStartIndex === undefined ? this._startRow : pStartIndex, + pRowCount === undefined ? this._pageSize : pRowCount, + (pTimeout ? pTimeout : -1)); + } + else + { + return []; } } /** - * generates a part of the sql - * - * @param {String|String[]|SqlBuilder} pElement the element to append - * @param {String} [pPrefix] string to be added before pElement - * @param {String} [pPostfix] string to be added after pElement - * @param {Boolean} [pAutoJoin=false] if this is true and pElement is an array, it will be automatically <br/> - * joined together to a string - * @param {Boolean} [pUseSubselectAlias=false] if true the subselectAlias is added if the element is a subquery + * Sets the pagesize for paging * - * @ignore + * @param {Number} pPageSize + * @return {SqlBuilder} current SqlBuilder object */ -SqlBuilder._getStatement = function (pElement, pPrefix, pPostfix, pAutoJoin, pUseSubselectAlias) +SqlBuilder.prototype.pageSize = function (pPageSize) { - var preparedValues = []; - if (typeof pElement !== "string") - { - if (Array.isArray(pElement) && pElement.length !== undefined && pAutoJoin) //array of fields - { - for (let i = 0; i < pElement.length; i++) - { - if (typeof pElement[i] !== "string") - pElement[i] = _getElement(pElement[i]); - } - - pElement = ArrayUtils.joinNonEmptyFields(pElement, ", "); - } - else - { - pElement = _getElement(pElement); - } - } - - if (pPrefix && pElement) - pElement = pPrefix + " " + pElement; - if (pPostfix && pElement) - pElement += " " + pPostfix; - - return { - preparedValues: preparedValues, - sqlStorage: pElement.toString() - }; - - function _getElement (element) - { - var isSubQuery = false; - var subselectAlias = ""; - if (SqlBuilder.checkCanBuildSql(element)) - { - if (element instanceof SqlBuilder && element.isFullSelect()) - { - isSubQuery = true; - - if (pUseSubselectAlias && element._subselectAlias) - subselectAlias = " " + element._subselectAlias; - } - element = element.build(); - } - preparedValues = preparedValues.concat(element[1]); - if (isSubQuery || pAutoJoin) - return "(" + element[0] + ")" + subselectAlias; - return element[0]; - } + this._pageSize = pPageSize; + return this; } /** - * builds a prepared condition out of the object. Only the condition is used. Select, from, ... are ignored. + * Sets the start row for paging * - * @return {PreparedSqlArray} prepared condition + * @param {Number} pStartRow + * @return {SqlBuilder} current SqlBuilder object */ -SqlBuilder.prototype.buildCondition = function() -{ - return [this._where.sqlStorage, this._where.preparedValues]; +SqlBuilder.prototype.startRow = function (pStartRow) +{ + this._startRow = pStartRow; + return this; } /** - * builds a prepared statement out of the object. If a part doesn't exit, it's just ignored. + * Executes the SQL and returns the result. The startRow for paging will be increased by the pageSize, so you can use this method + * for iterating over the table pages. You can use SqlBuilder.prototype.hasMoreRows() to check if the end of rows was reached. * - * @param {String} [pDefaultConditionIfNone=""] a default condition string which should be used if the SqlBuilder doesn't have any condition - * @return {PreparedSqlArray} prepared statement + * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned + * @param {Number} [pTimeout=-1] + * @return {String[][]} the result of the query */ -SqlBuilder.prototype.build = function(pDefaultConditionIfNone) +SqlBuilder.prototype.nextTablePage = function (pExecuteOnlyIfConditionExists, pTimeout) { - var wherePrefix = ""; - var fromObj = this._from; + if (this._pageSize == null || isNaN(this._pageSize)) + throw SqlBuilder._ERROR_PAGESIZE_INVALID(); - if (this.isFullSelect()) + if (this._startRow == null) + this._startRow = 0; + + if (this._hasMoreRows && this._checkForSelect(pExecuteOnlyIfConditionExists)) { - fromObj = { - sqlStorage: "from " + this._from.sqlStorage, - preparedValues: this._from.preparedValues - }; - if (this._where.sqlStorage) - wherePrefix = "where "; + var data = this.tablePage(this._startRow, this._pageSize, pExecuteOnlyIfConditionExists, pTimeout); + if (data.length < this._pageSize) + this._hasMoreRows = false; + this._startRow += this._pageSize; + return data; } - - var whereSql = this._where.sqlStorage; - - if (!this.hasCondition() && pDefaultConditionIfNone) - whereSql = wherePrefix + pDefaultConditionIfNone; - - var whereObj = { - sqlStorage : wherePrefix + whereSql, - preparedValues : this._where.preparedValues - }; - - var allParts = [ - this._select, - fromObj - ].concat(this._joins).concat([ - whereObj, - this._groupBy, - this._having, - this._orderBy - ]).concat(this._unions); - - var sqlStr = ""; - var preparedVals = []; - for (let i = 0, l = allParts.length; i < l; i++) + else { - let part = allParts[i]; - if (part) - { - if (sqlStr && part.sqlStorage) - sqlStr += " "; - sqlStr += part.sqlStorage; - if (part.preparedValues.length) - preparedVals = preparedVals.concat(part.preparedValues); - } + this._hasMoreRows = false; + return []; } - - return [sqlStr, preparedVals]; } /** - * Updates data in the database.<br/> - * Note: the default for pExecuteOnlyIfConditionExists is true to prevent updating all rows if the SqlBuilder has no condition. + * @return {Boolean} whether there are rows left for paging + */ +SqlBuilder.prototype.hasMoreRows = function () +{ + return this._hasMoreRows; +} + +/** + * Executes the SQL with paging and executes the given callback-function for every resultset until the last row has been reached or the function + * returns false. * - * @param {Boolean} [pExecuteOnlyIfConditionExists=true] If true, the update is only done if there is a condition.<br/> - * <strong>IMPORTANT: If this is set to false and there is no condition, every row in the table will be updated!</strong> - * @param {String} [pTableName] The table for updating data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, - * the table of the first where-condition is used. - * @param {String[]} pColumns The columns where you want to update. - * @param {SQLTYPES[]} [pColumnTypes=null] normally you can set this to null as the types are calculated if not provided - * @param {String[]} pValues The values to be updated. - * @param {Number} [pTimeout=-1] - * @return {Number} the number of rows affected - * @throws {Error} if no table is defined + * @param {Function} pCallBackFn CallBack-Function to execute for every page. If the function returns false, the execution will be stopped. + * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned + * @param {Number} [pTimeout=-1] */ -SqlBuilder.prototype.updateData = function(pExecuteOnlyIfConditionExists, pTableName, pColumns, pColumnTypes, pValues, pTimeout) +SqlBuilder.prototype.forEachPage = function (pCallBackFn, pExecuteOnlyIfConditionExists, pTimeout) { - if (this._checkForUpdate(pExecuteOnlyIfConditionExists)) + if (typeof pCallBackFn !== "function") + throw SqlBuilder._ERROR_NOT_A_FUNCTION(); + + var run = true; + var idx = 0; + while (run && this.hasMoreRows()) { - if (!pTableName && !this._tableName) - throw SqlBuilder._ERROR_NO_TABLE(); - - if (!pColumns) - pColumns = null; - - return db.updateData( - (pTableName ? pTableName : this._tableName), - pColumns, - pColumnTypes, - pValues, - this.buildCondition(), - (this.alias ? this.alias : db.getCurrentAlias()), - (pTimeout ? pTimeout : -1)); + run = pCallBackFn.call(null, this.nextTablePage(pExecuteOnlyIfConditionExists, pTimeout), idx++) != false; } - - return 0; } /** - * Updates data in the database. This function calls SqlBuilder.prototype.updateData, but provides a shorter syntax to - * improve the readability. + * Sets an impossible where-condition, so that the query won't return any rows. * - * @param {Object|Map} pFieldValues Object with the columns to update as keys mapped to their values - * @param {String} [pTableName] The table for updating data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, - * the table of the first where-condition is used. - * @return {Number} the number of rows affected - * @example - * newWhere("SALESORDER.SALESORDERID", "$field.SALESORDERID") - * .updateFields({"ORDERSTATUS" : "1"}); //pTableName can be omitted here since it's clearly defined by the given condition + * @return {SqlBuilder} current object */ -SqlBuilder.prototype.updateFields = function (pFieldValues, pTableName) +SqlBuilder.prototype.noResult = function () { - if (!pFieldValues || typeof(pFieldValues) !== "object") - throw SqlBuilder._ERROR_UPDATE_VALUES_INVALID; - - var columnValues = SqlBuilder._columnsValuesFromObject(pFieldValues, true); - if (columnValues.columns.length === 0) - return 0; - return this.updateData(true, pTableName, columnValues.columns, null, columnValues.values); + return this.clearWhere().where(SqlBuilder.NORESULT_CONDITION()); } /** - * Builds an array containing the table and condition for an update. - * - * @param {Object|Map} pFieldValues Object with the columns to update as keys mapped to their values - * @param {String} [pTableName] The table for updating data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, - * the table of the first where-condition is used. - * @return {Array} array of [tableName, columns, columnTypes, values, preparedCondition], like it is required by db.updates or null if there is no condition - * @example - * var updateStatements = []; - * updateStatements.push(newWhere("PERSON.PERSONID", pPersonId).buildUpdateStatement({"FIRSTNAME" : firstName})); - * updateStatements.push(newWhere("ORGANISATION.ORGANISATIONID", pOrganisationId).buildUpdateStatement({"NAME" : organisationName})); - * db.updates(updateStatements); + * checks if an update /delete statement should be called or not + * @return {Boolean} + * @private */ -SqlBuilder.prototype.buildUpdateStatement = function (pFieldValues, pTableName) +SqlBuilder.prototype._checkForUpdate = function(pExecuteOnlyIfConditionExists) { - if (!pFieldValues || typeof(pFieldValues) !== "object") - throw SqlBuilder._ERROR_UPDATE_VALUES_INVALID; - - var columnValues = SqlBuilder._columnsValuesFromObject(pFieldValues, true); - if (columnValues.columns.length !== 0 && this._checkForUpdate()) - { - if (!pTableName && !this._tableName) - throw SqlBuilder._ERROR_NO_TABLE(); - - return [ - (pTableName ? pTableName : this._tableName), - columnValues.columns, - null, - columnValues.values, - this.buildCondition() - ]; - } - return null; + if (pExecuteOnlyIfConditionExists === undefined) + pExecuteOnlyIfConditionExists = true; + + if (typeof pExecuteOnlyIfConditionExists !== "boolean") + throw SqlBuilder._ERROR_NOT_BOOLEAN(); + + return !pExecuteOnlyIfConditionExists || this.hasCondition(); } /** - * Builds an array containing the data for an insert. - * - * @param {Object|Map} pFieldValues Object with the columns to insert into as keys mapped to their values - * @param {String} [pTableName] The table for updating data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, - * the table of the first where-condition is used. - * @param {String} [pAutoUidField] UID column that should be filled with a random UUID - * @return {Array} array of [tableName, columns, columnTypes, values, preparedCondition], like it is required by db.updates or null if there is no condition + * checks if a select statement should be called or not + * @return {Boolean} + * @private */ -SqlBuilder.prototype.buildInsertStatement = function (pFieldValues, pTableName, pAutoUidField) +SqlBuilder.prototype._checkForSelect = function(pExecuteOnlyIfConditionExists) { - if (!pFieldValues || !Utils.isObject(pFieldValues)) - throw SqlBuilder._ERROR_UPDATE_VALUES_INVALID; - - if (pAutoUidField) - pFieldValues[pAutoUidField] = util.getNewUUID(); + if (pExecuteOnlyIfConditionExists == undefined) + pExecuteOnlyIfConditionExists = false; - var columnValues = SqlBuilder._columnsValuesFromObject(pFieldValues); - if (columnValues.columns.length !== 0) + if (typeof pExecuteOnlyIfConditionExists !== "boolean") + throw SqlBuilder._ERROR_NOT_BOOLEAN(); + + if (this.isFullSelect()) { - if (!pTableName && !this._tableName) - throw SqlBuilder._ERROR_NO_TABLE(); - - return [ - (pTableName ? pTableName : this._tableName), - columnValues.columns, - null, - columnValues.values - ]; + return !pExecuteOnlyIfConditionExists || this.hasCondition(); + } + else + { + throw SqlBuilder._ERROR_INCOMPLETE_SELECT(); } - return null; } /** - * Inserts data in the database. This function doesn't require any where-condition, it is intended to be called right after 'new SqlBuilder()'. <br/> + * translates SqlBuilder to plain SQL. Use this if prepared statements are not supported. + * For the db-functions (db.table, db.cell, etc.) use ".build()" as they support prepared statements. + * It resolves all prepared values. + * @param {String} [pAlias=undefined] the alias to use for db.translateStatement + * @return {String} plain SQL statement * - * @param {String} [pTableName] The table for inserting data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, - * the table of the first where-condition is used. - * @param {String[]} pColumns The columns where you want to insert into. - * @param {SQLTYPES[]} [pColumnTypes=null] normally you can set this to null as the types are calculated if not provided - * @param {String[]} pValues The values to be inserted. - * @param {Number} [pTimeout=-1] - * @return {Number} the number of rows affected - * @throws {Error} if no table is defined + * @deprecated use .toString() */ -SqlBuilder.prototype.insertData = function(pTableName, pColumns, pColumnTypes, pValues, pTimeout) +SqlBuilder.prototype.translate = function(pAlias) { - if (!pTableName && !this._tableName) - throw SqlBuilder._ERROR_NO_TABLE(); - - if (!pColumns) - pColumns = null; - - return db.insertData( - (pTableName ? pTableName : this._tableName), - pColumns, - pColumnTypes, - pValues, - (this.alias ? this.alias : db.getCurrentAlias()), - (pTimeout ? pTimeout : -1)); + return SqlUtils.translateStatementWithQuotes(this.build(), pAlias); } /** - * Inserts data in the database. This function calls SqlBuilder.prototype.insertData, but provides a shorter syntax to - * improve the readability. + * Creates an object for building a case-when statement. * - * @param {Object|Map} pFieldValues Object with the columns to update as keys mapped to their values - * @param {String} [pTableName] The table for updating data. If undefined, the from part of the SqlBuilder will be used (works only if it is a tablename). If no from is set, - * the table of the first where-condition is used. - * @param {String} [pAutoUidField] UID column that should be filled with a random UUID - * @return {Number} the number of rows affected - * @example - * new SqlBuilder().insertFields({ - * "ACTIVITY_ID" : pActivityId, - * "OBJECT_ROWID" : pRowId, - * "OBJECT_TYPE" : pObjectType - * }, "ACTIVITYLINK", "ACTIVITYLINKID"); + * @param {String|String[]|SqlBuilder|PreparedSqlArray} [pFieldOrCond] If this is the only parameter, it is used as Subselect <br/> + * else it is used as Field. <br/> + * Please see .where() for more information and examples. + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value whitch is used for the condition.<br/> + * Basically it can be nearly everything you need.<br/> + * Please see .where() for more information and examples. + * @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/> + * Please see .where() for more information and examples. + * + * @return {SqlBuilder._CaseWhen} */ -SqlBuilder.prototype.insertFields = function (pFieldValues, pTableName, pAutoUidField) +SqlBuilder.caseWhen = function (pFieldOrCond, pValue, pCondition, pFieldType) { - if (!pFieldValues || typeof(pFieldValues) !== "object") - throw SqlBuilder._ERROR_UPDATE_VALUES_INVALID; - - if (pAutoUidField) - pFieldValues[pAutoUidField] = util.getNewUUID(); - - var columnValues = SqlBuilder._columnsValuesFromObject(pFieldValues); - if (columnValues.columns.length === 0) - return 0; - return this.insertData(pTableName, columnValues.columns, null, columnValues.values); + return new SqlBuilder._CaseStatement().when(pFieldOrCond, pValue, pCondition, pFieldType); } -SqlBuilder._columnsValuesFromObject = function (pFieldValues, pIncludeNullValues) +/** + * @return {SqlBuilder._CaseStatement} + */ +SqlBuilder.caseStatement = function () { - var columns = []; - var values = []; - if (Utils.isMap(pFieldValues)) - { - pFieldValues.forEach(function (value, key) - { - if (pIncludeNullValues || (value !== undefined && value !== null)) - { - columns.push(key); - values.push(_valueToString(value)); - } - }); - } - else - { - for (let field in pFieldValues) - { - if (pIncludeNullValues || (pFieldValues[field] !== undefined && pFieldValues[field] !== null)) - { - columns.push(field); - values.push(_valueToString(pFieldValues[field])); - } - } - } - return { - columns: columns, - values: values - }; - - function _valueToString (pValue) - { - if (pValue === undefined || pValue === null) - return ""; - return pValue.toString(); - } + return new SqlBuilder._CaseStatement(); } /** - * Deletes data from the database.<br/> - * Note: the default for pExecuteOnlyIfConditionExists is true to prevent updating all rows if the SqlBuilder has no condition. - - * @param {Boolean} [pExecuteOnlyIfConditionExists=true] If true, the deletion is only done if there is a condition.<br/> - * <strong>IMPORTANT: If this is set to false and there is no condition, every row in the table will be deleted!</strong> - * @param {String} [pTableName] The table for deleting data. If undefined, the from part of the SqlBuilder will be used. If no from is set, - * the table of the first where-condition is used. - * @param {Number} [pTimeout=-1] - * @return {Number} the number of rows affected - * @throws {Error} if no table is defined - * @example - * newWhere("AB_ATTRIBUTE.AB_ATTRIBUTEID", "$local.uid") - * .deleteData(); //pTableName can be omitted here since it's clearly defined by the given condition + * Represents a case-when statement */ -SqlBuilder.prototype.deleteData = function(pExecuteOnlyIfConditionExists, pTableName, pTimeout) +SqlBuilder._CaseStatement = function () { - if (this._checkForUpdate(pExecuteOnlyIfConditionExists)) - { - if (!pTableName && !this._tableName) - throw SqlBuilder._ERROR_NO_TABLE(); - - return db.deleteData( - (pTableName ? pTableName : this._tableName), - this.buildCondition(), - (this.alias ? this.alias : db.getCurrentAlias()), - (pTimeout ? pTimeout : -1)); - } - else - { - return 0; - } + this._whenCondition = null; + this._whenThens = []; + this._elseValue = null; + this._afterWhenMask = new SqlBuilder._CaseWhen(this); } +SqlBuilder.defineCanBuildSql(SqlBuilder._CaseStatement.prototype); + /** - * Builds an array containing the table and condition for a delete. + * @param {String|String[]|SqlBuilder|PreparedSqlArray} [pFieldOrCond] If this is the only parameter, it is used as Subselect <br/> + * else it is used as Field. <br/> + * Please see .where() for more information and examples. + * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value whitch is used for the condition.<br/> + * Basically it can be nearly everything you need.<br/> + * Please see .where() for more information and examples. + * @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/> + * Please see .where() for more information and examples. * - * @param {Boolean} [pOnlyIfConditionExists=true] If true and there is no condition, null is returned.<br/> - * <strong>IMPORTANT: If this is set to false and there is no condition, every row in the table will be deleted!</strong> - * @param {String} [pTableName] The table for deleting data. If undefined, the from part of the SqlBuilder will be used. If no from is set, - * the table of the first where-condition is used. - * @return {Array} array of [tableName, preparedCondition], like it is required by db.deletes - * @example - * var deleteStatements = []; - * deleteStatements.push(newWhere("PERSON.PERSONID", pPersonId).buildDeleteStatement()); - * deleteStatements.push(newWhere("CONTACT.CONTACTID", pContactId).buildDeleteStatement()); - * db.deletes(deleteStatements); + * @return {SqlBuilder._CaseWhen} */ -SqlBuilder.prototype.buildDeleteStatement = function(pOnlyIfConditionExists, pTableName) +SqlBuilder._CaseStatement.prototype.when = function (pFieldOrCond, pValue, pCondition, pFieldType) { - if (this._checkForUpdate(pOnlyIfConditionExists)) - { - if (!pTableName && !this._tableName) - throw SqlBuilder._ERROR_NO_TABLE(); - - return [ - (pTableName ? pTableName : this._tableName), - this.buildCondition() - ]; - } - else - return null; + this._whenCondition = newWhere(pFieldOrCond, pValue, pCondition, pFieldType); + return this._afterWhenMask; } /** - * Executes the SQL using db.cell and returns the result.<br/> - * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. + * Sets the expression used for the else-part * - * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, "" or the provided FallbackValue is returned - * @param {AnyValue} [pFallbackValue=""] here you can provide a fallback value if pExecuteOnlyIfConditionExists is true and the SqlBuilder has no condition.<br/> - * This is intended for e.g. select count(*) from ... because there a default value of "0" is more helpful - * @return {String} the result of the query + * @param {String|SqlBuilder} pValue else-value + * @return {SqlBuilder._CaseStatement} */ -SqlBuilder.prototype.cell = function(pExecuteOnlyIfConditionExists, pFallbackValue) +SqlBuilder._CaseStatement.prototype.elseValue = function (pValue) { - if (this._checkForSelect(pExecuteOnlyIfConditionExists)) - { - return db.cell(this.build(), - (this.alias ? this.alias : db.getCurrentAlias())); - } - else - { - return (pFallbackValue ? pFallbackValue : ""); - } + this._elseValue = pValue; + return this; } /** - * Executes the SQL using db.array(db.ROW, ...) and returns the result.<br/> - * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. + * Sets the value used for the else-part, but wraps the value in '' * - * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned - * @param {Number} [pMaxRows=0] - * @param {Number} [pTimeout=-1] - * @return {String[]} the result of the query + * @param {String} pValue else-value + * @return {SqlBuilder._CaseStatement} */ -SqlBuilder.prototype.arrayRow = function (pExecuteOnlyIfConditionExists, pMaxRows, pTimeout) +SqlBuilder._CaseStatement.prototype.elseString = function (pValue) { - return this.array(db.ROW, pExecuteOnlyIfConditionExists, pMaxRows, pTimeout); + return this.elseValue("'" + pValue + "'"); } /** - * Executes the SQL using db.array(db.COLUMN, ...) and returns the result.<br/> - * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. - * - * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned - * @param {Number} [pMaxRows=0] - * @param {Number} [pTimeout=-1] - * @return {String[]} the result of the query + * @return {String} the case-when expression */ -SqlBuilder.prototype.arrayColumn = function (pExecuteOnlyIfConditionExists, pMaxRows, pTimeout) +SqlBuilder._CaseStatement.prototype.toString = function (pAlias) { - return this.array(db.COLUMN, pExecuteOnlyIfConditionExists, pMaxRows, pTimeout); + return db.translateStatement(this.build(), pAlias || db.getCurrentAlias()); } -/** - * Executes the SQL using db.array and returns the result.<br/> - * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. - * - * @param {Number} pType db.ROW or db.COLUMN - * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned - * @param {Number} [pMaxRows=0] - * @param {Number} [pTimeout=-1] - * @return {String[]} the result of the query - */ -SqlBuilder.prototype.array = function(pType, pExecuteOnlyIfConditionExists, pMaxRows, pTimeout) +SqlBuilder._CaseStatement.prototype.build = function (pParameters) { - if (this._checkForSelect(pExecuteOnlyIfConditionExists)) + var caseStatement = ["case"]; + var preparedValues = []; + this._whenThens.forEach(function (whenThen) { - return db.array(pType, this.build(), - (this.alias ? this.alias : db.getCurrentAlias()), - (pMaxRows ? pMaxRows : 0), - (pTimeout ? pTimeout : -1)); - } - else + var when = SqlBuilder._getStatement(whenThen.condition, "when"); + var then = SqlBuilder._getStatement(whenThen.thenValue, "then"); + caseStatement.push(when.sqlStorage); + caseStatement.push(then.sqlStorage); + preparedValues = preparedValues.concat(when.preparedValues, then.preparedValues); + }); + if (this._elseValue) { - return []; + let elseStatement = SqlBuilder._getStatement(this._elseValue, "else"); + caseStatement.push(elseStatement.sqlStorage); + preparedValues = preparedValues.concat(elseStatement.preparedValues); } + caseStatement.push("end"); + + return [ + caseStatement.join(" "), + preparedValues + ]; } /** - * Executes the SQL using db.arrayPage and returns the result.<br/> - * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. - * - * @param {Number} pType db.ROW or db.COLUMN - * @param {Number} pStartIndex - * @param {Number} pRowCount - * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned - * @param {Number} [pTimeout=-1] - * @return {String[]} the result of the query + * Custom toJSON method that omits the property '_afterWhenMask', because cyclic references can't be stringified */ -SqlBuilder.prototype.arrayPage = function(pType, pStartIndex, pRowCount, pExecuteOnlyIfConditionExists, pTimeout) +SqlBuilder._CaseStatement.prototype.toJSON = function () { - if (this._checkForSelect(pExecuteOnlyIfConditionExists)) - { - return db.arrayPage(pType, this.build(), - (this.alias ? this.alias : db.getCurrentAlias()), - pStartIndex === undefined ? this._startRow : pStartIndex, - pRowCount === undefined ? this._pageSize : pRowCount, - (pTimeout ? pTimeout : -1)); - } - else - { - return []; - } + return { + _whenCondition: this._whenCondition, + _whenThens: this._whenThens, + _elseValue: this._elseValue + }; } /** - * Executes the SQL using db.table and returns the result.<br/> - * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. - * - * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned - * @param {Number} [pMaxRows=0] - * @param {Number} [pTimeout=-1] - * @return {String[][]} the result of the query + * Object providing the then-methods for the case-when expression. It can be only be accessed after calling .when to ensure a 'then' + * can only be added after a 'when'. */ -SqlBuilder.prototype.table = function(pExecuteOnlyIfConditionExists, pMaxRows, pTimeout) +SqlBuilder._CaseWhen = function (pCaseStatement) { - if (this._checkForSelect(pExecuteOnlyIfConditionExists)) - { - return db.table(this.build(), - (this.alias ? this.alias : db.getCurrentAlias()), - (pMaxRows ? pMaxRows : 0), - (pTimeout ? pTimeout : -1)); - } - else - { - return []; - } -} - -/** - * Executes the SQL using db.tablePage and returns the result.<br/> - * Note: the default for pExecuteOnlyIfConditionExists is false becausse it is more natural to select all rows if no condition exists. - * - * @param {Number} pStartIndex - * @param {Number} pRowCount - * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned - * @param {Number} [pTimeout=-1] - * @return {String[][]} the result of the query - */ -SqlBuilder.prototype.tablePage = function(pStartIndex, pRowCount, pExecuteOnlyIfConditionExists, pTimeout) -{ - if (this._checkForSelect(pExecuteOnlyIfConditionExists)) - { - return db.tablePage(this.build(), - (this.alias ? this.alias : db.getCurrentAlias()), - pStartIndex === undefined ? this._startRow : pStartIndex, - pRowCount === undefined ? this._pageSize : pRowCount, - (pTimeout ? pTimeout : -1)); - } - else - { - return []; - } + this._caseStatement = pCaseStatement; } /** - * Sets the pagesize for paging + * Sets the expression for the then * - * @param {Number} pPageSize - * @return {SqlBuilder} current SqlBuilder object + * @param {String|SqlBuilder} pValue then-value + * @return {SqlBuilder._CaseStatement} */ -SqlBuilder.prototype.pageSize = function (pPageSize) +SqlBuilder._CaseWhen.prototype.then = function (pValue) { - this._pageSize = pPageSize; - return this; + var condition = this._caseStatement._whenCondition; + this._caseStatement._whenCondition = null; + this._caseStatement._whenThens.push({condition: condition, thenValue: pValue}); + return this._caseStatement; } /** - * Sets the start row for paging + * Sets the value for the then, but wraps the value in '' * - * @param {Number} pStartRow - * @return {SqlBuilder} current SqlBuilder object + * @param {String} pValue then-value + * @return {SqlBuilder._CaseStatement} */ -SqlBuilder.prototype.startRow = function (pStartRow) +SqlBuilder._CaseWhen.prototype.thenString = function (pValue) { - this._startRow = pStartRow; - return this; + return this.then("'" + pValue + "'"); } /** - * Executes the SQL and returns the result. The startRow for paging will be increased by the pageSize, so you can use this method - * for iterating over the table pages. You can use SqlBuilder.prototype.hasMoreRows() to check if the end of rows was reached. + *provides functions for masking sql functions + * + * @param {String} [pAlias=currentAlias] database alias, you can specify null if you have no alias available and you can manually set the dbType property * - * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned - * @param {Number} [pTimeout=-1] - * @return {String[][]} the result of the query + * @class */ -SqlBuilder.prototype.nextTablePage = function (pExecuteOnlyIfConditionExists, pTimeout) +function SqlMaskingUtils (pAlias) { - if (this._pageSize == null || isNaN(this._pageSize)) - throw SqlBuilder._ERROR_PAGESIZE_INVALID(); - - if (this._startRow == null) - this._startRow = 0; + this.alias = null; + Object.defineProperty(this, "alias", { + set: function(v){ + this._alias = v; + if (v != null) + this._dbType = db.getDatabaseType(this._alias); + }, + get: function(){ + return this._alias; + } + }); + this.dbType = null; + //provide the possibility to just set dbType (e.g. for testing) with no association to an alias + Object.defineProperty(this, "dbType", { + set: function(v){ + this._alias = null; + this._dbType = v; + }, + get: function(){ + return this._dbType; + } + }); - if (this._hasMoreRows && this._checkForSelect(pExecuteOnlyIfConditionExists)) - { - var data = this.tablePage(this._startRow, this._pageSize, pExecuteOnlyIfConditionExists, pTimeout); - if (data.length < this._pageSize) - this._hasMoreRows = false; - this._startRow += this._pageSize; - return data; - } + if (pAlias === undefined) + this.alias = vars.getString("$sys.dbalias"); else - { - this._hasMoreRows = false; - return []; - } -} - -/** - * @return {Boolean} whether there are rows left for paging - */ -SqlBuilder.prototype.hasMoreRows = function () -{ - return this._hasMoreRows; + this.alias = pAlias; } /** - * Executes the SQL with paging and executes the given callback-function for every resultset until the last row has been reached or the function - * returns false. + * Returns the reminder of: arg1 / arg2 * - * @param {Function} pCallBackFn CallBack-Function to execute for every page. If the function returns false, the execution will be stopped. - * @param {Boolean} [pExecuteOnlyIfConditionExists=false] if true and there is no condition, [] is returned - * @param {Number} [pTimeout=-1] + * @param {string|SqlBuilder} pField1 arg1 + * @param {string|SqlBuilder} pField2 arg2 + * + * @returns the reminder of the division */ -SqlBuilder.prototype.forEachPage = function (pCallBackFn, pExecuteOnlyIfConditionExists, pTimeout) +SqlMaskingUtils.prototype.modulo = function(pField1, pField2) { - if (typeof pCallBackFn !== "function") - throw SqlBuilder._ERROR_NOT_A_FUNCTION(); - - var run = true; - var idx = 0; - while (run && this.hasMoreRows()) + switch(this.dbType) { - run = pCallBackFn.call(null, this.nextTablePage(pExecuteOnlyIfConditionExists, pTimeout), idx++) != false; + case db.DBTYPE_ORACLE10_CLUSTER: + case db.DBTYPE_ORACLE10_OCI: + case db.DBTYPE_ORACLE10_THIN: + case db.DBTYPE_DERBY10: + case db.DBTYPE_POSTGRESQL8: + return "mod((" + pField1.toString() + "), (" + pField2.toString() + "))"; + case db.DBTYPE_SQLSERVER2000: + case db.DBTYPE_MARIADB10: + case db.DBTYPE_MYSQL4: + return "((" + pField1.toString() + ") % (" + pField2.toString() + "))"; } } /** - * Sets an impossible where-condition, so that the query won't return any rows. + * Returns an sql expression resolving into the diffrence between 2 dates in days + * The expression is a s follows: first_date - second_date + * So if the first date is smaller than the second date you will get a negative value returned * - * @return {SqlBuilder} current object + * @param {string|SqlBuilder} pDateField1 the first date + * @param {string|SqlBuilder} pDateField2 the second date + * + * @returns the diffrence between the two dates in days */ -SqlBuilder.prototype.noResult = function () +SqlMaskingUtils.prototype.dayDateDifference = function(pDateField1, pDateField2) { - return this.clearWhere().where(SqlBuilder.NORESULT_CONDITION()); -} - -/** - * checks if an update /delete statement should be called or not - * @return {Boolean} - * @private - */ -SqlBuilder.prototype._checkForUpdate = function(pExecuteOnlyIfConditionExists) -{ - if (pExecuteOnlyIfConditionExists === undefined) - pExecuteOnlyIfConditionExists = true; + var maskingUtils = new SqlMaskingUtils(); - if (typeof pExecuteOnlyIfConditionExists !== "boolean") - throw SqlBuilder._ERROR_NOT_BOOLEAN(); + var date1 = maskingUtils.cast("(" + pDateField1.toString() + ")", SQLTYPES.DATE); + var date2 = maskingUtils.cast("(" + pDateField2.toString() + ")", SQLTYPES.DATE); - return !pExecuteOnlyIfConditionExists || this.hasCondition(); + switch(this.dbType) + { + case db.DBTYPE_DERBY10: + // JDBC escape systax is required for timestampdiff + return "{fn timestampdiff(SQL_TSI_DAY, " + date2 + ", " + date1 + ")}"; + case db.DBTYPE_POSTGRESQL8: + case db.DBTYPE_SQLSERVER2000: + return "datediff(day, " + date1 + ", " + date2 + ")"; + case db.DBTYPE_MYSQL4: + case db.DBTYPE_MARIADB10: + return "datediff(" + date1 + ", " + date2 + ")"; + case db.DBTYPE_ORACLE10_CLUSTER: + case db.DBTYPE_ORACLE10_OCI: + case db.DBTYPE_ORACLE10_THIN: + return "(" + date1 + " - " + date2 + ")"; + } } /** - * checks if a select statement should be called or not - * @return {Boolean} - * @private + * returns the concat symbol depending on database type + * @return {String} Concat Symbol */ -SqlBuilder.prototype._checkForSelect = function(pExecuteOnlyIfConditionExists) +SqlMaskingUtils.prototype.getConcatSymbol = function() { - if (pExecuteOnlyIfConditionExists == undefined) - pExecuteOnlyIfConditionExists = false; - - if (typeof pExecuteOnlyIfConditionExists !== "boolean") - throw SqlBuilder._ERROR_NOT_BOOLEAN(); - - if (this.isFullSelect()) - { - return !pExecuteOnlyIfConditionExists || this.hasCondition(); - } - else + switch(this.dbType) { - throw SqlBuilder._ERROR_INCOMPLETE_SELECT(); + case db.DBTYPE_SQLSERVER2000: + return " + "; + case db.DBTYPE_MARIADB10: + case db.DBTYPE_MYSQL4: + case db.DBTYPE_ORACLE10_CLUSTER: + case db.DBTYPE_ORACLE10_THIN: + case db.DBTYPE_ORACLE10_OCI: + case db.DBTYPE_POSTGRESQL8: + case db.DBTYPE_DERBY10: + default: + return " || "; } } + /** - * generates a part of the sql - * - * @param {String|String[]|SqlBuilder|SqlCondition} pElement the element to append - * @param {String} [pPrefix] string to be added before pElement - * @param {Boolean} [pAutoJoin] if this is true and pElement is an array, it will be automatically - * joined together to a string - * - * @private - * @deprecated this method is only needed by deprecated methods - */ -SqlBuilder.prototype._getClause = function (pElement, pPrefix, pAutoJoin) +* Returns the group_concat function, which groups <br> +* multiple Row Values into one Cell. Note: This function <br> +* does not work on Derby<br> +* +* @param {String} pField <p> +* Expression that shall be grouped.<br> +* @param {String} pSeperator <p> +* Character that shall be used as Seperator<br> +* @return {String} <p> +* Returns the field with groupConcat wrapped around<br> +*/ +SqlMaskingUtils.prototype.getGroupConcat = function(pField, pSeperator) { - var preparedValues = []; - if (typeof pElement !== "string") - { - if (pElement.length !== undefined && pAutoJoin) //array of fields - { - for (let i = 0, l = pElement.length; i < l; i++) - { - if (typeof pElement[i] !== "string") - pElement[i] = _getElement(pElement[i]); - } - pElement = pElement.join(", "); - } - else - { - pElement = _getElement(pElement); - } - } - - if (pPrefix && pElement) - pElement = pPrefix + " " + pElement; - - return [pElement.toString(), preparedValues]; + var group; + if(pField == null || pSeperator == null || pField == null && pSeperator == null) + throw new Error(translate.withArguments("Field or Seperator were empty function: %0", ["SqlMaskingUtils.prototype.getGroupConcat"])); - function _getElement (element) + switch(this.dbType) { - if (element instanceof SqlBuilder || element instanceof SqlCondition) - element = element.build(); - preparedValues = preparedValues.concat(element[1]); - if (element instanceof SqlBuilder || pAutoJoin) - return "(" + element[0] + ")"; - return element[0]; + case db.DBTYPE_MARIADB10: + case db.DBTYPE_MYSQL4: + group = " GROUP_CONCAT("+pField+" SEPARATOR "+pSeperator+")"; + break; + case db.DBTYPE_ORACLE10_CLUSTER: + case db.DBTYPE_ORACLE10_THIN: + case db.DBTYPE_ORACLE10_OCI: + case db.DBTYPE_POSTGRESQL8: + case db.DBTYPE_SQLSERVER2000: + group = " STRING_AGG("+pField+", "+pSeperator+")"; + break; + case db.DBTYPE_DERBY10: + logging.log(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.getGroupConcat"]), logging.ERROR); + break; } + return group; } /** - * translates SqlBuilder to plain SQL. Use this if prepared statements are not supported. - * For the db-functions (db.table, db.cell, etc.) use ".build()" as they support prepared statements. - * It resolves all prepared values. - * @param {String} [pAlias=undefined] the alias to use for db.translateStatement - * @return {String} plain SQL statement - * - * @deprecated use .toString() - */ -SqlBuilder.prototype.translate = function(pAlias) -{ - return SqlUtils.translateStatementWithQuotes(this.build(), pAlias); -} - -/** - * Creates an object for building a case-when statement. - * - * @param {String|String[]|SqlBuilder|PreparedSqlArray} [pFieldOrCond] If this is the only parameter, it is used as Subselect <br/> - * else it is used as Field. <br/> - * Please see .where() for more information and examples. - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value whitch is used for the condition.<br/> - * Basically it can be nearly everything you need.<br/> - * Please see .where() for more information and examples. - * @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/> - * Please see .where() for more information and examples. - * - * @return {SqlBuilder._CaseWhen} - */ -SqlBuilder.caseWhen = function (pFieldOrCond, pValue, pCondition, pFieldType) +* Returns the trim function, which removes the<br> +* leading and trailing spaces in a string, depending<br> +* on the database behind the given alias note that<br> +* this function does not verifiy where the types of<br> +* your expression are trimable or not.<br> +* +* @param {String} pField <p> +* Expression that shall be trimmed.<br> +* @return {String} <p> +* Returns the trimmed string.<br> +*/ +SqlMaskingUtils.prototype.trim = function (pField) { - return new SqlBuilder._CaseStatement().when(pFieldOrCond, pValue, pCondition, pFieldType); + if (this.dbType == db.DBTYPE_SQLSERVER2000) + return "ltrim(rtrim(" + pField + "))"; + return "trim(" + pField + ")"; } /** - * @return {SqlBuilder._CaseStatement} - */ -SqlBuilder.caseStatement = function () +* returns the max-value sql expressions depending on the database behind the given alias +* note that this function does not verifiy if the field (and type) usage is valid at all +* +* @param {String} pField expression +* +* @return {String} sql-part that can be used in a select +*/ +SqlMaskingUtils.prototype.max = function (pField) { - return new SqlBuilder._CaseStatement(); + return "max(" + pField + ")"; } /** - * Represents a case-when statement - */ -SqlBuilder._CaseStatement = function () +* returns the min-value sql expressions depending on the database behind the given alias +* note that this function does not verifiy if the field (and type) usage is valid at all +* +* @param {String} pField expression +* +* @return {String} sql-part that can be used in a select +*/ +SqlMaskingUtils.prototype.min = function (pField) { - this._whenCondition = null; - this._whenThens = []; - this._elseValue = null; - this._afterWhenMask = new SqlBuilder._CaseWhen(this); + return "min(" + pField + ")"; } -SqlBuilder.defineCanBuildSql(SqlBuilder._CaseStatement.prototype); - /** - * @param {String|String[]|SqlBuilder|PreparedSqlArray} [pFieldOrCond] If this is the only parameter, it is used as Subselect <br/> - * else it is used as Field. <br/> - * Please see .where() for more information and examples. - * @param {String|SqlBuilder|PreparedSqlArray|Array|OtherTypes} [pValue] This is the value whitch is used for the condition.<br/> - * Basically it can be nearly everything you need.<br/> - * Please see .where() for more information and examples. - * @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/> - * Please see .where() for more information and examples. - * - * @return {SqlBuilder._CaseWhen} + * masks the function cast of standard sql + * please note that this function does not do any validation if it's possible to cast the expression's datatype you pass to the function in every supported DBMS + * + * Problems: + * Derby has problems with casting to CHAR({> 254}) https://db.apache.org/derby/docs/10.14/ref/rrefsqlj13733.html + * + * @param {String} pField name of the database field that shall be castet + * @param {String} [pTargetDatatype] a SQLTYPES-value of the following: SQLTYPES.CHAR, SQLTYPES.VARCHAR, SQLTYPES.INTEGER, + * SQLTYPES.DECIMAL, SQLTYPES.DATE + * @param {int|int[]} pTargetLength specifies the length of the target data type; + * <br/>- char/varchar: length + * <br/>- decimal: [length, decimals] + * + * @return {String} sql part to be included in sql-statements */ -SqlBuilder._CaseStatement.prototype.when = function (pFieldOrCond, pValue, pCondition, pFieldType) +SqlMaskingUtils.prototype.cast = function (pField, pTargetDatatype, pTargetLength) { - this._whenCondition = newWhere(pFieldOrCond, pValue, pCondition, pFieldType); - return this._afterWhenMask; -} + /* Some information if you want to add supported databaseTypes or dataTypes: + * You should consider using the _mapDefaults function-expression (details in the functions doc) + * However you shouldn't use the function in a "default"-Block of a switch-case because of the following behaviour: + * If a datatype is not supported you just have to NOT specify "sqlDataType" (leave it "undefined") -> an error is then raised + * Therefore you should explicitly define which Data-type is supported and which is not + */ + var sqlDataType; + var functionName = "cast";//overwrite this in the "switch (dbType)" if needed with your DBMS -/** - * Sets the expression used for the else-part - * - * @param {String|SqlBuilder} pValue else-value - * @return {SqlBuilder._CaseStatement} - */ -SqlBuilder._CaseStatement.prototype.elseValue = function (pValue) -{ - this._elseValue = pValue; - return this; -} - -/** - * Sets the value used for the else-part, but wraps the value in '' - * - * @param {String} pValue else-value - * @return {SqlBuilder._CaseStatement} - */ -SqlBuilder._CaseStatement.prototype.elseString = function (pValue) -{ - return this.elseValue("'" + pValue + "'"); -} + /** + * handles default-scenarios for mapping input-targetDatatype to a string for a sql-statement + * e.g. SQLTYPES.INTEGER --> int + * @param {Number} dataType input as a value of "SQLTYPES." that will be mapped to a string + * @return {String} the mapped dataType for using in a sql-statement + */ + var _mapDefaults = function (dataType) + { + switch (dataType) + { + case SQLTYPES.CHAR: + return "char"; + case SQLTYPES.VARCHAR: + return "char"; + case SQLTYPES.NVARCHAR: + return "nvarchar"; + case SQLTYPES.INTEGER: + return "int"; + case SQLTYPES.DECIMAL: + return "decimal"; + case SQLTYPES.DATE: + return "date"; + case SQLTYPES.TIMESTAMP: + return "timestamp"; + } + return null; + } + + switch (this.dbType) + { + case db.DBTYPE_DERBY10: + switch(pTargetDatatype) + { + case SQLTYPES.NVARCHAR: + case SQLTYPES.VARCHAR: + // Because of a Derby bug, you can't cast INTEGER into VARCHAR + // Therefor first cast to char then to varchar + // https://issues.apache.org/jira/browse/DERBY-2072 + // This cast to char is only done if the length is not bigger than 254, + // otherwise the additional cast would result in a different error + if (pTargetLength <= 254) + pField = "rtrim(" + this.cast(pField, SQLTYPES.CHAR, pTargetLength) + ")"; + sqlDataType = "varchar"; + break; + case SQLTYPES.CHAR: + sqlDataType = "char"; + break; + case SQLTYPES.DECIMAL: + case SQLTYPES.INTEGER: + case SQLTYPES.DATE: + case SQLTYPES.TIMESTAMP: + sqlDataType = _mapDefaults(pTargetDatatype); + break; + } + break; + case db.DBTYPE_MARIADB10: + case db.DBTYPE_MYSQL4: + switch(pTargetDatatype) + { + case SQLTYPES.TIMESTAMP: + sqlDataType = "datetime"; + break; + case SQLTYPES.NVARCHAR: + case SQLTYPES.VARCHAR: + case SQLTYPES.CHAR: + case SQLTYPES.INTEGER: + case SQLTYPES.DECIMAL: + case SQLTYPES.DATE: + sqlDataType = _mapDefaults(pTargetDatatype); + break; + } + break; + case db.DBTYPE_ORACLE10_CLUSTER: + case db.DBTYPE_ORACLE10_THIN: + case db.DBTYPE_ORACLE10_OCI: + switch(pTargetDatatype) + { + case SQLTYPES.VARCHAR: + sqlDataType = "varchar2"; + break; + case SQLTYPES.NVARCHAR: + sqlDataType = "nvarchar2"; + break; + case SQLTYPES.INTEGER: + sqlDataType = "number"; + pTargetLength = "10" + break; + case SQLTYPES.CHAR: + case SQLTYPES.DECIMAL: + case SQLTYPES.DATE: + case SQLTYPES.TIMESTAMP: + sqlDataType = _mapDefaults(pTargetDatatype); + break; + } + break; + case db.DBTYPE_POSTGRESQL8: + switch(pTargetDatatype) + { + case SQLTYPES.DATE: + case SQLTYPES.TIMESTAMP: + case SQLTYPES.DECIMAL: + case SQLTYPES.INTEGER: + case SQLTYPES.CHAR: + case SQLTYPES.VARCHAR: + case SQLTYPES.NVARCHAR: + sqlDataType = _mapDefaults(pTargetDatatype); + break; + } + break; + case db.DBTYPE_SQLSERVER2000: + switch(pTargetDatatype) + { + case SQLTYPES.TIMESTAMP: + sqlDataType = "datetime"; + break; + case SQLTYPES.DATE: + case SQLTYPES.DECIMAL: + case SQLTYPES.INTEGER: + case SQLTYPES.CHAR: + case SQLTYPES.VARCHAR: + case SQLTYPES.NVARCHAR: + sqlDataType = _mapDefaults(pTargetDatatype); + break; + } + //TODO: firebird support? + } -/** - * @return {String} the case-when expression - */ -SqlBuilder._CaseStatement.prototype.toString = function (pAlias) -{ - return db.translateStatement(this.build(), pAlias || db.getCurrentAlias()); -} + if (sqlDataType == undefined) + throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.cast._mapDefaults"])); -SqlBuilder._CaseStatement.prototype.build = function (pParameters) -{ - var caseStatement = ["case"]; - var preparedValues = []; - this._whenThens.forEach(function (whenThen) - { - var when = SqlBuilder._getStatement(whenThen.condition, "when"); - var then = SqlBuilder._getStatement(whenThen.thenValue, "then"); - caseStatement.push(when.sqlStorage); - caseStatement.push(then.sqlStorage); - preparedValues = preparedValues.concat(when.preparedValues, then.preparedValues); - }); - if (this._elseValue) + if (pTargetLength == undefined) + pTargetLength = ""; + else if (pTargetLength != "") { - let elseStatement = SqlBuilder._getStatement(this._elseValue, "else"); - caseStatement.push(elseStatement.sqlStorage); - preparedValues = preparedValues.concat(elseStatement.preparedValues); + if (Array.isArray(pTargetLength)) + pTargetLength = "(" + pTargetLength.join(", ") + ")"; + else + pTargetLength = "(" + pTargetLength + ")"; } - caseStatement.push("end"); - - return [ - caseStatement.join(" "), - preparedValues - ]; -} -/** - * Custom toJSON method that omits the property '_afterWhenMask', because cyclic references can't be stringified - */ -SqlBuilder._CaseStatement.prototype.toJSON = function () -{ - return { - _whenCondition: this._whenCondition, - _whenThens: this._whenThens, - _elseValue: this._elseValue - }; + return functionName + "(" + pField + " as " + sqlDataType + pTargetLength + ")"; } /** - * Object providing the then-methods for the case-when expression. It can be only be accessed after calling .when to ensure a 'then' - * can only be added after a 'when'. + * masks the cast function for lob datatypes(clob, blob) into varchar or similar + * + * @param {String} pField expression that shall be casted + * @param {Number|Number[]} pTargetLength desired length of the datatype + * decimal: [length, decimals] + * + * @return {String} part of sql-expression that can be used */ -SqlBuilder._CaseWhen = function (pCaseStatement) +SqlMaskingUtils.prototype.castLob = function (pField, pTargetLength) { - this._caseStatement = pCaseStatement; + switch (this.dbType) + { + case db.DBTYPE_ORACLE10_CLUSTER: + case db.DBTYPE_ORACLE10_THIN: + case db.DBTYPE_ORACLE10_OCI: + return "DBMS_LOB.SUBSTR(" + pField + ", " + pTargetLength + ", 1)"; + default: + return this.cast(pField, SQLTYPES.VARCHAR, pTargetLength); + } } /** - * Sets the expression for the then - * - * @param {String|SqlBuilder} pValue then-value - * @return {SqlBuilder._CaseStatement} + * returns the function which determines the length of binary data + * + * @param {String} pField name of the checked field + * + * @return {String} */ -SqlBuilder._CaseWhen.prototype.then = function (pValue) +SqlMaskingUtils.prototype.binDataLength = function (pField) { - var condition = this._caseStatement._whenCondition; - this._caseStatement._whenCondition = null; - this._caseStatement._whenThens.push({condition: condition, thenValue: pValue}); - return this._caseStatement; + switch (this.dbType) + { + case db.DBTYPE_MARIADB10: + case db.DBTYPE_MYSQL4: + case db.DBTYPE_ORACLE10_CLUSTER: + case db.DBTYPE_ORACLE10_THIN: + case db.DBTYPE_ORACLE10_OCI: + case db.DBTYPE_POSTGRESQL8: + case db.DBTYPE_DERBY10: + return "length(" + pField + ")"; + case db.DBTYPE_SQLSERVER2000: + return "datalength(" + pField + ")"; + default: + throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.binDataLength"])); + } } /** - * Sets the value for the then, but wraps the value in '' - * - * @param {String} pValue then-value - * @return {SqlBuilder._CaseStatement} - */ -SqlBuilder._CaseWhen.prototype.thenString = function (pValue) + * masks the sql function substring + * + * @param {String } pField the expression that shall be substringed + * @param {Number} pStartPos posistion where the substring starts + * @param {Number} pLength amount of characters of the expression will be returned by the sql function + * + * @return {String} part of sql-expression that can be used for substringing + */ +SqlMaskingUtils.prototype.substring = function (pField, pStartPos, pLength) { - return this.then("'" + pValue + "'"); -} - -/** - *provides functions for masking sql functions - * - * @param {String} [pAlias=currentAlias] database alias, you can specify null if you have no alias available and you can manually set the dbType property - * - * @class - */ -function SqlMaskingUtils (pAlias) -{ - this.alias = null; - Object.defineProperty(this, "alias", { - set: function(v){ - this._alias = v; - if (v != null) - this._dbType = db.getDatabaseType(this._alias); - }, - get: function(){ - return this._alias; - } - }); - this.dbType = null; - //provide the possibility to just set dbType (e.g. for testing) with no association to an alias - Object.defineProperty(this, "dbType", { - set: function(v){ - this._alias = null; - this._dbType = v; - }, - get: function(){ - return this._dbType; - } - }); - - if (pAlias === undefined) - this.alias = vars.getString("$sys.dbalias"); - else - this.alias = pAlias; -} - -/** - * Returns the reminder of: arg1 / arg2 - * - * @param {string|SqlBuilder} pField1 arg1 - * @param {string|SqlBuilder} pField2 arg2 - * - * @returns the reminder of the division - */ -SqlMaskingUtils.prototype.modulo = function(pField1, pField2) -{ - switch(this.dbType) + var sqlFnName; + switch (this.dbType) { case db.DBTYPE_ORACLE10_CLUSTER: - case db.DBTYPE_ORACLE10_OCI: case db.DBTYPE_ORACLE10_THIN: + case db.DBTYPE_ORACLE10_OCI: + sqlFnName = "substr"; + break; case db.DBTYPE_DERBY10: + sqlFnName = "substr"; + break; case db.DBTYPE_POSTGRESQL8: - return "mod((" + pField1.toString() + "), (" + pField2.toString() + "))"; - case db.DBTYPE_SQLSERVER2000: - case db.DBTYPE_MARIADB10: - case db.DBTYPE_MYSQL4: - return "((" + pField1.toString() + ") % (" + pField2.toString() + "))"; - } -} - -/** - * Returns an sql expression resolving into the diffrence between 2 dates in days - * The expression is a s follows: first_date - second_date - * So if the first date is smaller than the second date you will get a negative value returned - * - * @param {string|SqlBuilder} pDateField1 the first date - * @param {string|SqlBuilder} pDateField2 the second date - * - * @returns the diffrence between the two dates in days - */ -SqlMaskingUtils.prototype.dayDateDifference = function(pDateField1, pDateField2) -{ - var maskingUtils = new SqlMaskingUtils(); - - var date1 = maskingUtils.cast("(" + pDateField1.toString() + ")", SQLTYPES.DATE); - var date2 = maskingUtils.cast("(" + pDateField2.toString() + ")", SQLTYPES.DATE); - - switch(this.dbType) - { - case db.DBTYPE_DERBY10: - // JDBC escape systax is required for timestampdiff - return "{fn timestampdiff(SQL_TSI_DAY, " + date2 + ", " + date1 + ")}"; - case db.DBTYPE_POSTGRESQL8: + sqlFnName = "substr"; + break; case db.DBTYPE_SQLSERVER2000: - return "datediff(day, " + date1 + ", " + date2 + ")"; + sqlFnName = "substring"; + break; case db.DBTYPE_MYSQL4: case db.DBTYPE_MARIADB10: - return "datediff(" + date1 + ", " + date2 + ")"; - case db.DBTYPE_ORACLE10_CLUSTER: - case db.DBTYPE_ORACLE10_OCI: - case db.DBTYPE_ORACLE10_THIN: - return "(" + date1 + " - " + date2 + ")"; + sqlFnName = "substring"; + break; + default: + throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.substring"])); } + return sqlFnName + "(" + pField + ", " + pStartPos + ", " + pLength + ")"; } /** - * returns the concat symbol depending on database type - * @return {String} Concat Symbol - */ -SqlMaskingUtils.prototype.getConcatSymbol = function() +* masks the function concat (without separator) +* +* @param {Array} pFields fields (or expressions) that should be concatenated +* +* @return {String} part of SQL-querey +*/ +SqlMaskingUtils.prototype.concatenate = function (pFields) { - switch(this.dbType) + if (pFields.length === 0) + return "''"; + + switch (this.dbType) { - case db.DBTYPE_SQLSERVER2000: - return " + "; - case db.DBTYPE_MARIADB10: case db.DBTYPE_MYSQL4: + case db.DBTYPE_MARIADB10: + case db.DBTYPE_POSTGRESQL8: + return " concat(" + pFields.join(", ") + ")"; case db.DBTYPE_ORACLE10_CLUSTER: case db.DBTYPE_ORACLE10_THIN: case db.DBTYPE_ORACLE10_OCI: - case db.DBTYPE_POSTGRESQL8: + break; + case db.DBTYPE_SQLSERVER2000: + //MS SQL Server supports "concat_ws" (and ignoring null values) from version SQL Server 2017 and newer: + //https://docs.microsoft.com/de-de/sql/t-sql/functions/concat-ws-transact-sql?view=sql-server-2017 + break; case db.DBTYPE_DERBY10: + break; default: - return " || "; + throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.concatenate"])); + } + + var fields = []; + + for (let i = 0; i < pFields.length; i++) + { + let field = pFields[i]; + let isLast = i + 1 === pFields.length; + + if (field && field != "''") + { + if (_isFixedValue(field)) + fields.push(field); + else + fields.push(this.isNull(field)); + } + } + + return fields.join(this.getConcatSymbol()) || "''"; + + function _isFixedValue (pSqlField) + { + return pSqlField.startsWith("'") && pSqlField.endsWith("'") && !pSqlField.slice(1, -1).includes("'"); } } - /** -* Returns the group_concat function, which groups <br> -* multiple Row Values into one Cell. Note: This function <br> -* does not work on Derby<br> +* masks the function concat_ws +* if a sql field is empty no separator will be added +* note that this function will often create a lot of sql-code * -* @param {String} pField <p> -* Expression that shall be grouped.<br> -* @param {String} pSeperator <p> -* Character that shall be used as Seperator<br> -* @return {String} <p> -* Returns the field with groupConcat wrapped around<br> +* @param {Array} pFields fields (or expressions) that should be concatenated +* @param {String} [pSeparator=space-character] character for separating the fields +* @param {String} [pAutoTrimFields=true] autoTrimFields if true the expressions are always trimmed, false no change will be applied +* +* @return {String} part of SQL-querey */ -SqlMaskingUtils.prototype.getGroupConcat = function(pField, pSeperator) +SqlMaskingUtils.prototype.concatWithSeparator = function (pFields, pSeparator, pAutoTrimFields) { - var group; - if(pField == null || pSeperator == null || pField == null && pSeperator == null) - throw new Error(translate.withArguments("Field or Seperator were empty function: %0", ["SqlMaskingUtils.prototype.getGroupConcat"])); + if (pFields.length === 0) + return "''"; + if (pFields.length === 1) + return pFields[0]; - switch(this.dbType) + if (pSeparator === "" && pAutoTrimFields == false) + return this.concatenate(pFields); + + if (pAutoTrimFields == undefined) + pAutoTrimFields = true; + + if (pSeparator === null || pSeparator === undefined) + pSeparator = "' '"; + else if (pSeparator || pSeparator === "") + pSeparator = "'" + db.quote(pSeparator, this.alias) + "'"; + + var isEmptyStringNull = false; + + switch (this.dbType) { - case db.DBTYPE_MARIADB10: case db.DBTYPE_MYSQL4: - group = " GROUP_CONCAT("+pField+" SEPARATOR "+pSeperator+")"; - break; + case db.DBTYPE_MARIADB10: + case db.DBTYPE_POSTGRESQL8: + if (pAutoTrimFields) + pFields = pFields.map(this.trim, this); + return " concat_ws(" + pSeparator + ", " + pFields.join(", ") + ")"; case db.DBTYPE_ORACLE10_CLUSTER: case db.DBTYPE_ORACLE10_THIN: case db.DBTYPE_ORACLE10_OCI: - case db.DBTYPE_POSTGRESQL8: + isEmptyStringNull = true; //empty strings are changed to DB-null-values internally in oracle + break; case db.DBTYPE_SQLSERVER2000: - group = " STRING_AGG("+pField+", "+pSeperator+")"; + //MS SQL Server supports "concat_ws" (and ignoring null values) from version SQL Server 2017 and newer: + //https://docs.microsoft.com/de-de/sql/t-sql/functions/concat-ws-transact-sql?view=sql-server-2017 break; case db.DBTYPE_DERBY10: - logging.log(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.getGroupConcat"]), logging.ERROR); break; + default: + throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.concatWithSeparator"])); } - return group; -} + + var concatCharacter = this.getConcatSymbol(); + var concatSql = ""; + + for (let i = 0; i < pFields.length; i++) + { + let field = pFields[i]; + let isLast = i + 1 === pFields.length; + + if (_isFixedValue(field)) + concatSql += (pAutoTrimFields ? "'" + field.slice(1, -1).trim() + "'" : field); + else + { + let stringField = isEmptyStringNull ? field : this.isNull(field); + concatSql += (pAutoTrimFields ? this.trim(stringField) : this.isNull(stringField)); + } + + if (!isLast) + { + concatSql += concatCharacter; + let nextField = pFields[i+1]; + if (pSeparator && _isFixedValue(nextField)) + { + if (nextField.slice(1, -1).trim()) + concatSql += pSeparator + concatCharacter; + } + else if (pSeparator) + { + let nextNotNullCondition; + let nextFieldTrimmed = pAutoTrimFields ? this.trim(nextField) : nextField; + if (isEmptyStringNull) + nextNotNullCondition = nextFieldTrimmed + " is not null"; + else + nextNotNullCondition = nextField + " is not null and " + nextFieldTrimmed + " != ''"; -/** -* Returns the trim function, which removes the<br> -* leading and trailing spaces in a string, depending<br> -* on the database behind the given alias note that<br> -* this function does not verifiy where the types of<br> -* your expression are trimable or not.<br> -* -* @param {String} pField <p> -* Expression that shall be trimmed.<br> -* @return {String} <p> -* Returns the trimmed string.<br> -*/ -SqlMaskingUtils.prototype.trim = function (pField) -{ - if (this.dbType == db.DBTYPE_SQLSERVER2000) - return "ltrim(rtrim(" + pField + "))"; - return "trim(" + pField + ")"; -} - -/** -* returns the max-value sql expressions depending on the database behind the given alias -* note that this function does not verifiy if the field (and type) usage is valid at all -* -* @param {String} pField expression -* -* @return {String} sql-part that can be used in a select -*/ -SqlMaskingUtils.prototype.max = function (pField) -{ - return "max(" + pField + ")"; -} - -/** -* returns the min-value sql expressions depending on the database behind the given alias -* note that this function does not verifiy if the field (and type) usage is valid at all -* -* @param {String} pField expression -* -* @return {String} sql-part that can be used in a select -*/ -SqlMaskingUtils.prototype.min = function (pField) -{ - return "min(" + pField + ")"; + concatSql += "case when " + nextNotNullCondition + " then " + pSeparator + " else '' end " + concatCharacter; + } + } + } + + return concatSql; + + function _isFixedValue (pSqlField) + { + return pSqlField.startsWith("'") && pSqlField.endsWith("'") && !pSqlField.slice(1, -1).includes("'"); + } } /** - * masks the function cast of standard sql - * please note that this function does not do any validation if it's possible to cast the expression's datatype you pass to the function in every supported DBMS - * - * Problems: - * Derby has problems with casting to CHAR({> 254}) https://db.apache.org/derby/docs/10.14/ref/rrefsqlj13733.html - * - * @param {String} pField name of the database field that shall be castet - * @param {String} [pTargetDatatype] a SQLTYPES-value of the following: SQLTYPES.CHAR, SQLTYPES.VARCHAR, SQLTYPES.INTEGER, - * SQLTYPES.DECIMAL, SQLTYPES.DATE - * @param {int|int[]} pTargetLength specifies the length of the target data type; - * <br/>- char/varchar: length - * <br/>- decimal: [length, decimals] + * get the current timestamp * - * @return {String} sql part to be included in sql-statements + * @return {String} sql expression for the current timestamp */ -SqlMaskingUtils.prototype.cast = function (pField, pTargetDatatype, pTargetLength) +SqlMaskingUtils.prototype.currentTimestamp = function() { - /* Some information if you want to add supported databaseTypes or dataTypes: - * You should consider using the _mapDefaults function-expression (details in the functions doc) - * However you shouldn't use the function in a "default"-Block of a switch-case because of the following behaviour: - * If a datatype is not supported you just have to NOT specify "sqlDataType" (leave it "undefined") -> an error is then raised - * Therefore you should explicitly define which Data-type is supported and which is not - */ - var sqlDataType; - var functionName = "cast";//overwrite this in the "switch (dbType)" if needed with your DBMS - - /** - * handles default-scenarios for mapping input-targetDatatype to a string for a sql-statement - * e.g. SQLTYPES.INTEGER --> int - * @param {Number} dataType input as a value of "SQLTYPES." that will be mapped to a string - * @return {String} the mapped dataType for using in a sql-statement - */ - var _mapDefaults = function (dataType) - { - switch (dataType) - { - case SQLTYPES.CHAR: - return "char"; - case SQLTYPES.VARCHAR: - return "char"; - case SQLTYPES.NVARCHAR: - return "nvarchar"; - case SQLTYPES.INTEGER: - return "int"; - case SQLTYPES.DECIMAL: - return "decimal"; - case SQLTYPES.DATE: - return "date"; - case SQLTYPES.TIMESTAMP: - return "timestamp"; - } - return null; - } - - switch (this.dbType) + let now; + switch(this.dbType) { - case db.DBTYPE_DERBY10: - switch(pTargetDatatype) - { - case SQLTYPES.NVARCHAR: - case SQLTYPES.VARCHAR: - // Because of a Derby bug, you can't cast INTEGER into VARCHAR - // Therefor first cast to char then to varchar - // https://issues.apache.org/jira/browse/DERBY-2072 - // This cast to char is only done if the length is not bigger than 254, - // otherwise the additional cast would result in a different error - if (pTargetLength <= 254) - pField = "rtrim(" + this.cast(pField, SQLTYPES.CHAR, pTargetLength) + ")"; - sqlDataType = "varchar"; - break; - case SQLTYPES.CHAR: - sqlDataType = "char"; - break; - case SQLTYPES.DECIMAL: - case SQLTYPES.INTEGER: - case SQLTYPES.DATE: - case SQLTYPES.TIMESTAMP: - sqlDataType = _mapDefaults(pTargetDatatype); - break; - } - break; case db.DBTYPE_MARIADB10: + now = "NOW()" + break; case db.DBTYPE_MYSQL4: - switch(pTargetDatatype) - { - case SQLTYPES.TIMESTAMP: - sqlDataType = "datetime"; - break; - case SQLTYPES.NVARCHAR: - case SQLTYPES.VARCHAR: - case SQLTYPES.CHAR: - case SQLTYPES.INTEGER: - case SQLTYPES.DECIMAL: - case SQLTYPES.DATE: - sqlDataType = _mapDefaults(pTargetDatatype); - break; - } + now = "CURRENT_TIMESTAMP()"; break; case db.DBTYPE_ORACLE10_CLUSTER: case db.DBTYPE_ORACLE10_THIN: case db.DBTYPE_ORACLE10_OCI: - switch(pTargetDatatype) - { - case SQLTYPES.VARCHAR: - sqlDataType = "varchar2"; - break; - case SQLTYPES.NVARCHAR: - sqlDataType = "nvarchar2"; - break; - case SQLTYPES.INTEGER: - sqlDataType = "number"; - pTargetLength = "10" - break; - case SQLTYPES.CHAR: - case SQLTYPES.DECIMAL: - case SQLTYPES.DATE: - case SQLTYPES.TIMESTAMP: - sqlDataType = _mapDefaults(pTargetDatatype); - break; - } + now = "SYSTIMESTAMP"; break; case db.DBTYPE_POSTGRESQL8: - switch(pTargetDatatype) - { - case SQLTYPES.DATE: - case SQLTYPES.TIMESTAMP: - case SQLTYPES.DECIMAL: - case SQLTYPES.INTEGER: - case SQLTYPES.CHAR: - case SQLTYPES.VARCHAR: - case SQLTYPES.NVARCHAR: - sqlDataType = _mapDefaults(pTargetDatatype); - break; - } + now = "LOCALTIMESTAMP"; break; case db.DBTYPE_SQLSERVER2000: - switch(pTargetDatatype) - { - case SQLTYPES.TIMESTAMP: - sqlDataType = "datetime"; - break; - case SQLTYPES.DATE: - case SQLTYPES.DECIMAL: - case SQLTYPES.INTEGER: - case SQLTYPES.CHAR: - case SQLTYPES.VARCHAR: - case SQLTYPES.NVARCHAR: - sqlDataType = _mapDefaults(pTargetDatatype); - break; - } - //TODO: firebird support? - } - - if (sqlDataType == undefined) - throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.cast._mapDefaults"])); - - if (pTargetLength == undefined) - pTargetLength = ""; - else if (pTargetLength != "") - { - if (Array.isArray(pTargetLength)) - pTargetLength = "(" + pTargetLength.join(", ") + ")"; - else - pTargetLength = "(" + pTargetLength + ")"; + now = "GETDATE()"; + break; + case db.DBTYPE_DERBY10: + now = "CURRENT_TIMESTAMP"; + break; + default: + throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.currentTimestamp"])); + } - - return functionName + "(" + pField + " as " + sqlDataType + pTargetLength + ")"; + return now; } + /** - * masks the cast function for lob datatypes(clob, blob) into varchar or similar - * - * @param {String} pField expression that shall be casted - * @param {Number|Number[]} pTargetLength desired length of the datatype - * decimal: [length, decimals] - * - * @return {String} part of sql-expression that can be used + * masks the function which will generate a new UUID + * <br> + * <p> + * Note: + * The function is not supported for the DB-Types Derby and PostgreSQL <br> + * When using a postreSQL DB ensure that the module 'uuid-ossp' is implemented otherwise it's not supported + * </p> + * + * @return {String} sql expression that creates a new UUID */ -SqlMaskingUtils.prototype.castLob = function (pField, pTargetLength) +SqlMaskingUtils.prototype.newUUID = function() { - switch (this.dbType) + let uuID; + switch(this.dbType) { + case db.DBTYPE_MYSQL4: + case db.DBTYPE_MARIADB10: + uuID = "UUID()"; + break; case db.DBTYPE_ORACLE10_CLUSTER: case db.DBTYPE_ORACLE10_THIN: case db.DBTYPE_ORACLE10_OCI: - return "DBMS_LOB.SUBSTR(" + pField + ", " + pTargetLength + ", 1)"; - default: - return this.cast(pField, SQLTYPES.VARCHAR, pTargetLength); - } -} - -/** - * returns the function which determines the length of binary data - * - * @param {String} pField name of the checked field - * - * @return {String} - */ -SqlMaskingUtils.prototype.binDataLength = function (pField) + uuID = "(select regexp_replace(rawtohex(sys_guid()), '([A-F0-9]{8})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{12})', '\1-\2-\3-\4-\5') as UUID from dual)"; + break; + case db.DBTYPE_POSTGRESQL8: + uuID = "uuid_generate_v4()";//be carefull: to run this fuction the PostgreSQL module "uuid-ossp" has to be implemented + case db.DBTYPE_SQLSERVER2000: + uuID = "NEWID()"; + break; + case db.DBTYPE_DERBY10: + logging.log(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.uUID"]), logging.ERROR); + break; + default: + throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.uUID"])); + + } + return uuID; +} + +/** +* masks the function concat +* if a sql field is empty no separator will be added +* note that this function will often create a lot of sql-code +* +* @param {Array} pFields fields (or expressions) that should be concatenated +* @param {String} [pSeparator=space-character] character for separating the fields +* @param {String} [pAutoTrimFields=true] autoTrimFields if true the expressions are always trimmed, false no change will be applied +* +* @return {String} part of SQL-querey +* +* @deprecated The function has been renamed to SqlMaskingUtils.prototype.concatWithSeparator to differentiate it from +* SqlMaskingUtils.prototype.concatenate. +*/ +SqlMaskingUtils.prototype.concat = function (pFields, pSeparator, pAutoTrimFields) +{ + return this.concatWithSeparator(pFields, pSeparator, pAutoTrimFields); +} + +/** + * returns the function for replacing a null value + * + * @param {String} pField expression that shall be checked for a null value + * @param {String} [pReplacement=empty string] expression that shall be used if the field contains null + * + * @return {string} + */ +SqlMaskingUtils.prototype.isNull = function (pField, pReplacement) { + if (pReplacement == undefined) + pReplacement = "''"; + if (pField instanceof SqlBuilder) + pField = "(" + pField.toString() + ")"; switch (this.dbType) { - case db.DBTYPE_MARIADB10: - case db.DBTYPE_MYSQL4: + case db.DBTYPE_SQLSERVER2000: + return "isnull(" + pField + ", " + pReplacement + ")"; case db.DBTYPE_ORACLE10_CLUSTER: - case db.DBTYPE_ORACLE10_THIN: case db.DBTYPE_ORACLE10_OCI: + case db.DBTYPE_ORACLE10_THIN : + return "nvl(" + pField + ", " + pReplacement + ")"; case db.DBTYPE_POSTGRESQL8: case db.DBTYPE_DERBY10: - return "length(" + pField + ")"; - case db.DBTYPE_SQLSERVER2000: - return "datalength(" + pField + ")"; + case db.DBTYPE_MYSQL4: + case db.DBTYPE_MARIADB10: default: - throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.binDataLength"])); + return "coalesce(" + pField + ", " + pReplacement + ")"; } } /** - * masks the sql function substring - * - * @param {String } pField the expression that shall be substringed - * @param {Number} pStartPos posistion where the substring starts - * @param {Number} pLength amount of characters of the expression will be returned by the sql function - * - * @return {String} part of sql-expression that can be used for substringing - */ -SqlMaskingUtils.prototype.substring = function (pField, pStartPos, pLength) + * gets the day from a timestamp + * + * @param {String} pField timestamp to get the day from + * + * @return {String} sql expression that extracts the day from a timestamp + */ +SqlMaskingUtils.prototype.dayFromDate = function (pField) { - var sqlFnName; switch (this.dbType) { case db.DBTYPE_ORACLE10_CLUSTER: case db.DBTYPE_ORACLE10_THIN: case db.DBTYPE_ORACLE10_OCI: - sqlFnName = "substr"; - break; + return "to_char(" + pField + ",'dd')"; case db.DBTYPE_DERBY10: - sqlFnName = "substr"; - break; - case db.DBTYPE_POSTGRESQL8: - sqlFnName = "substr"; - break; case db.DBTYPE_SQLSERVER2000: - sqlFnName = "substring"; - break; case db.DBTYPE_MYSQL4: case db.DBTYPE_MARIADB10: - sqlFnName = "substring"; - break; - default: - throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.substring"])); + return "day(" + pField + ")"; + case db.DBTYPE_POSTGRESQL8: + return "extract (day from " + pField + ")"; } - return sqlFnName + "(" + pField + ", " + pStartPos + ", " + pLength + ")"; } /** -* masks the function concat (without separator) -* -* @param {Array} pFields fields (or expressions) that should be concatenated -* -* @return {String} part of SQL-querey -*/ -SqlMaskingUtils.prototype.concatenate = function (pFields) + * gets the month from a timestamp + * + * @param {String} pField timestamp to get the month from + * + * @return {String} sql expression that extracts the month from a timestamp + */ +SqlMaskingUtils.prototype.monthFromDate = function (pField) { - if (pFields.length === 0) - return "''"; - switch (this.dbType) { + case db.DBTYPE_ORACLE10_CLUSTER: + case db.DBTYPE_ORACLE10_THIN: + case db.DBTYPE_ORACLE10_OCI: + return "to_char(" + pField + ",'MM')"; + case db.DBTYPE_DERBY10: + case db.DBTYPE_SQLSERVER2000: case db.DBTYPE_MYSQL4: case db.DBTYPE_MARIADB10: + return "month(" + pField + ")"; case db.DBTYPE_POSTGRESQL8: - return " concat(" + pFields.join(", ") + ")"; + return "extract (month from " + pField + ")"; + } +} + +/** + * gets the year from a timestamp + * + * @param {String} pField timestamp to get the year from + * + * @return {String} sql expression that extracts the year from a timestamp + */ +SqlMaskingUtils.prototype.yearFromDate = function(pField) +{ + switch (this.dbType) + { case db.DBTYPE_ORACLE10_CLUSTER: case db.DBTYPE_ORACLE10_THIN: case db.DBTYPE_ORACLE10_OCI: - break; - case db.DBTYPE_SQLSERVER2000: - //MS SQL Server supports "concat_ws" (and ignoring null values) from version SQL Server 2017 and newer: - //https://docs.microsoft.com/de-de/sql/t-sql/functions/concat-ws-transact-sql?view=sql-server-2017 - break; + return "to_char(" + pField + ",'yyyy')"; case db.DBTYPE_DERBY10: - break; - default: - throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.concatenate"])); - } - - var fields = []; - - for (let i = 0; i < pFields.length; i++) - { - let field = pFields[i]; - let isLast = i + 1 === pFields.length; - - if (field && field != "''") - { - if (_isFixedValue(field)) - fields.push(field); - else - fields.push(this.isNull(field)); - } - } - - return fields.join(this.getConcatSymbol()) || "''"; - - function _isFixedValue (pSqlField) - { - return pSqlField.startsWith("'") && pSqlField.endsWith("'") && !pSqlField.slice(1, -1).includes("'"); + case db.DBTYPE_SQLSERVER2000: + case db.DBTYPE_MYSQL4: + case db.DBTYPE_MARIADB10: + return "YEAR(" + pField + ")"; + case db.DBTYPE_POSTGRESQL8: + return "EXTRACT (YEAR FROM " + pField + ")"; } } + /** -* masks the function concat_ws -* if a sql field is empty no separator will be added -* note that this function will often create a lot of sql-code -* -* @param {Array} pFields fields (or expressions) that should be concatenated -* @param {String} [pSeparator=space-character] character for separating the fields -* @param {String} [pAutoTrimFields=true] autoTrimFields if true the expressions are always trimmed, false no change will be applied -* -* @return {String} part of SQL-querey -*/ -SqlMaskingUtils.prototype.concatWithSeparator = function (pFields, pSeparator, pAutoTrimFields) + * gets the hour from a timestamp + * + * @param {String} pField timestamp to get the year from + * + * @return {String} sql expression that extracts the hour from a timestamp + */ +SqlMaskingUtils.prototype.hourFromDate = function(pField) { - if (pFields.length === 0) - return "''"; - if (pFields.length === 1) - return pFields[0]; - - if (pSeparator === "" && pAutoTrimFields == false) - return this.concatenate(pFields); - - if (pAutoTrimFields == undefined) - pAutoTrimFields = true; - - if (pSeparator === null || pSeparator === undefined) - pSeparator = "' '"; - else if (pSeparator || pSeparator === "") - pSeparator = "'" + db.quote(pSeparator, this.alias) + "'"; - - var isEmptyStringNull = false; - switch (this.dbType) { + case db.DBTYPE_ORACLE10_CLUSTER: + case db.DBTYPE_ORACLE10_THIN: + case db.DBTYPE_ORACLE10_OCI: + return "to_char(" + pField + ",'HH24')"; + case db.DBTYPE_DERBY10: case db.DBTYPE_MYSQL4: case db.DBTYPE_MARIADB10: + return "HOUR(" + pField + ")"; + case db.DBTYPE_SQLSERVER2000: + return "DATEPART(hour, "+ pField + ")"; case db.DBTYPE_POSTGRESQL8: - if (pAutoTrimFields) - pFields = pFields.map(this.trim, this); - return " concat_ws(" + pSeparator + ", " + pFields.join(", ") + ")"; + return "EXTRACT (HOUR FROM " + pField + ")"; + } +} + + +/** + * gets the last day of the month from a timestamp + * + * @param {String} pField timestamp to get the last day of the month from + * + * @return {String} sql expression that extracts the last day of the month from a timestamp + */ +SqlMaskingUtils.prototype.lastDayOfMonth = function(pField) +{ + switch (this.dbType) + { case db.DBTYPE_ORACLE10_CLUSTER: case db.DBTYPE_ORACLE10_THIN: case db.DBTYPE_ORACLE10_OCI: - isEmptyStringNull = true; //empty strings are changed to DB-null-values internally in oracle - break; - case db.DBTYPE_SQLSERVER2000: - //MS SQL Server supports "concat_ws" (and ignoring null values) from version SQL Server 2017 and newer: - //https://docs.microsoft.com/de-de/sql/t-sql/functions/concat-ws-transact-sql?view=sql-server-2017 - break; case db.DBTYPE_DERBY10: - break; - default: - throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.concatWithSeparator"])); - } - - var concatCharacter = this.getConcatSymbol(); - var concatSql = ""; + case db.DBTYPE_MYSQL4: + case db.DBTYPE_MARIADB10: + return "LAST_DAY(" + pField + ")"; + case db.DBTYPE_SQLSERVER2000: + return "DATEADD(MONTH, DATEDIFF(MONTH, 0 ," + pField + ") + 1, 0) -1"; + case db.DBTYPE_POSTGRESQL8: + return "DATE_TRUNC('month'," + pField + "::date) + interval '1 month' - interval '1 day')::date"; + } +} + + +/** + * returns the first field, that is not null or empty + * masks the behaviour of coalesce with case when + * + * @param pFields {Array} Array of fieldnames. Has to be in the right order. It will be checked from 0 upwards + */ +SqlMaskingUtils.prototype.coalesce = function(pFields) +{ + var retSql = ""; - for (let i = 0; i < pFields.length; i++) + if(pFields && typeof pFields == "object" && pFields.length) { - let field = pFields[i]; - let isLast = i + 1 === pFields.length; - - if (_isFixedValue(field)) - concatSql += (pAutoTrimFields ? "'" + field.slice(1, -1).trim() + "'" : field); - else + retSql = "case "; + + for(let i = 0; i < pFields.length; i++) { - let stringField = isEmptyStringNull ? field : this.isNull(field); - concatSql += (pAutoTrimFields ? this.trim(stringField) : this.isNull(stringField)); + retSql += " when (" + pFields[i] + " is not null or " + pFields[i] + " <> '') then " + pFields[i] + " " } - - if (!isLast) - { - concatSql += concatCharacter; - let nextField = pFields[i+1]; - if (pSeparator && _isFixedValue(nextField)) - { - if (nextField.slice(1, -1).trim()) - concatSql += pSeparator + concatCharacter; - } - else if (pSeparator) - { - let nextNotNullCondition; - let nextFieldTrimmed = pAutoTrimFields ? this.trim(nextField) : nextField; - if (isEmptyStringNull) - nextNotNullCondition = nextFieldTrimmed + " is not null"; - else - nextNotNullCondition = nextField + " is not null and " + nextFieldTrimmed + " != ''"; - concatSql += "case when " + nextNotNullCondition + " then " + pSeparator + " else '' end " + concatCharacter; - } - } + retSql += " else null end"; } + else + throw {message:"The input to coalesce has to be an Array containing the column names"}; - return concatSql; - - function _isFixedValue (pSqlField) - { - return pSqlField.startsWith("'") && pSqlField.endsWith("'") && !pSqlField.slice(1, -1).includes("'"); - } + return retSql; } /** - * get the current timestamp - * - * @return {String} sql expression for the current timestamp + * Converts day, month and year fields into a date + * The parameters can't be null!!! + * + * @param pYear {String} year field + * @param pMonth {String} month field + * @param pDay {String} day field */ -SqlMaskingUtils.prototype.currentTimestamp = function() +SqlMaskingUtils.prototype.makeDate = function(pYear, pMonth, pDay) { - let now; - switch(this.dbType) + switch(this.dbType) { - case db.DBTYPE_MARIADB10: - now = "NOW()" - break; - case db.DBTYPE_MYSQL4: - now = "CURRENT_TIMESTAMP()"; - break; + case db.DBTYPE_POSTGRESQL8: + return "make_date" + "(" + pYear + ", " + pMonth + ", " + pDay + ")"; + case db.DBTYPE_SQLSERVER2000: + return "datefromparts" + "(" + pYear + ", " + pMonth + ", " + pDay + ")"; + case db.DBTYPE_ORACLE10_CLUSTER: case db.DBTYPE_ORACLE10_THIN: case db.DBTYPE_ORACLE10_OCI: - now = "SYSTIMESTAMP"; - break; - case db.DBTYPE_POSTGRESQL8: - now = "LOCALTIMESTAMP"; - break; - case db.DBTYPE_SQLSERVER2000: - now = "GETDATE()"; - break; + return "to_date(lpad(" + pYear + ", 4, '0') || lpad(" + pMonth + ", 2, '0') || lpad(" + pYear + ", 2, '0'), 'YYYY-MM-DD')"; + + // from datestr without filled zeros e.g.: 2000-5-2 + case db.DBTYPE_MYSQL4: + case db.DBTYPE_MARIADB10: + var dateFields = [ + this.cast(pYear, SQLTYPES.CHAR, 4), + this.cast(pMonth, SQLTYPES.CHAR, 2), + this.cast(pDay, SQLTYPES.CHAR, 2) + ]; + var dateStr = this.concatWithSeparator(dateFields, "-", false); + return this.cast(dateStr, SQLTYPES.DATE); + + // from datestr with filled zeros e.g.: 2000-05-02 case db.DBTYPE_DERBY10: - now = "CURRENT_TIMESTAMP"; - break; - default: - throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.currentTimestamp"])); - + // maskingUtils: workaround + function _castWithZeros(maskingUtils, field, size) { + var casted = maskingUtils.cast(field, SQLTYPES.VARCHAR, size); + var concatenated = maskingUtils.concatenate(["'000'", casted]); + // + 1: derbys substr starts counting at 1, for whatever reason + return "substr(" + concatenated + ", length(" + concatenated + ") - " + size + " + 1)"; + } + var dateFieldsZeroed = [ + _castWithZeros(this, pYear, 4), + _castWithZeros(this, pMonth, 2), + _castWithZeros(this, pDay, 2) + ]; + var dateStrZeroed = this.concatWithSeparator(dateFieldsZeroed, "-", false); + return this.cast(dateStrZeroed, SQLTYPES.DATE); } - return now; } - /** - * masks the function which will generate a new UUID - * <br> - * <p> - * Note: - * The function is not supported for the DB-Types Derby and PostgreSQL <br> - * When using a postreSQL DB ensure that the module 'uuid-ossp' is implemented otherwise it's not supported - * </p> + * Returns the database equivalent of the current date function as String depending on the database type * - * @return {String} sql expression that creates a new UUID */ -SqlMaskingUtils.prototype.newUUID = function() +SqlMaskingUtils.prototype.currentDate = function() { - let uuID; - switch(this.dbType) + switch(this.dbType) { - case db.DBTYPE_MYSQL4: - case db.DBTYPE_MARIADB10: - uuID = "UUID()"; - break; + case db.DBTYPE_POSTGRESQL8: case db.DBTYPE_ORACLE10_CLUSTER: case db.DBTYPE_ORACLE10_THIN: case db.DBTYPE_ORACLE10_OCI: - uuID = "(select regexp_replace(rawtohex(sys_guid()), '([A-F0-9]{8})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{12})', '\1-\2-\3-\4-\5') as UUID from dual)"; - break; - case db.DBTYPE_POSTGRESQL8: - uuID = "uuid_generate_v4()";//be carefull: to run this fuction the PostgreSQL module "uuid-ossp" has to be implemented - case db.DBTYPE_SQLSERVER2000: - uuID = "NEWID()"; - break; + case db.DBTYPE_MARIADB10: case db.DBTYPE_DERBY10: - logging.log(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.uUID"]), logging.ERROR); - break; - default: - throw new Error(translate.withArguments("${SQL_LIB_UNSUPPORTED_DBTYPE} function: %0", ["SqlMaskingUtils.prototype.uUID"])); - + return "current_date"; + case db.DBTYPE_MYSQL4: + return "current_date()"; + case db.DBTYPE_SQLSERVER2000: + return "getdate()"; } - return uuID; -} - -/** -* masks the function concat -* if a sql field is empty no separator will be added -* note that this function will often create a lot of sql-code -* -* @param {Array} pFields fields (or expressions) that should be concatenated -* @param {String} [pSeparator=space-character] character for separating the fields -* @param {String} [pAutoTrimFields=true] autoTrimFields if true the expressions are always trimmed, false no change will be applied -* -* @return {String} part of SQL-querey -* -* @deprecated The function has been renamed to SqlMaskingUtils.prototype.concatWithSeparator to differentiate it from -* SqlMaskingUtils.prototype.concatenate. -*/ -SqlMaskingUtils.prototype.concat = function (pFields, pSeparator, pAutoTrimFields) -{ - return this.concatWithSeparator(pFields, pSeparator, pAutoTrimFields); } /** - * returns the function for replacing a null value - * - * @param {String} pField expression that shall be checked for a null value - * @param {String} [pReplacement=empty string] expression that shall be used if the field contains null - * - * @return {string} - */ -SqlMaskingUtils.prototype.isNull = function (pField, pReplacement) + * Returns the database equivalent of the fetch first row only function as String depending on the database type + * + */ +SqlMaskingUtils.prototype.limit = function(pRowAmount) { - if (pReplacement == undefined) - pReplacement = "''"; - if (pField instanceof SqlBuilder) - pField = "(" + pField.toString() + ")"; - switch (this.dbType) + switch(this.dbType) { + case db.DBTYPE_POSTGRESQL8: + case db.DBTYPE_MARIADB10: + case db.DBTYPE_MYSQL4: + return "LIMIT " + pRowAmount; case db.DBTYPE_SQLSERVER2000: - return "isnull(" + pField + ", " + pReplacement + ")"; + return "offset 0 rows fetch first " + pRowAmount + " rows only"; + case db.DBTYPE_DERBY10: case db.DBTYPE_ORACLE10_CLUSTER: + case db.DBTYPE_ORACLE10_THIN: case db.DBTYPE_ORACLE10_OCI: - case db.DBTYPE_ORACLE10_THIN : - return "nvl(" + pField + ", " + pReplacement + ")"; - case db.DBTYPE_POSTGRESQL8: - case db.DBTYPE_DERBY10: - case db.DBTYPE_MYSQL4: - case db.DBTYPE_MARIADB10: - default: - return "coalesce(" + pField + ", " + pReplacement + ")"; + return "fetch first " + pRowAmount + " rows only"; } } /** - * gets the day from a timestamp - * - * @param {String} pField timestamp to get the day from + * functions for various Sql-actions + * Do not create an instance of this! * - * @return {String} sql expression that extracts the day from a timestamp + * @class + * @static */ -SqlMaskingUtils.prototype.dayFromDate = function (pField) -{ - switch (this.dbType) - { - case db.DBTYPE_ORACLE10_CLUSTER: - case db.DBTYPE_ORACLE10_THIN: - case db.DBTYPE_ORACLE10_OCI: - return "to_char(" + pField + ",'dd')"; - case db.DBTYPE_DERBY10: - case db.DBTYPE_SQLSERVER2000: - case db.DBTYPE_MYSQL4: - case db.DBTYPE_MARIADB10: - return "day(" + pField + ")"; - case db.DBTYPE_POSTGRESQL8: - return "extract (day from " + pField + ")"; - } -} - -/** - * gets the month from a timestamp - * - * @param {String} pField timestamp to get the month from - * - * @return {String} sql expression that extracts the month from a timestamp - */ -SqlMaskingUtils.prototype.monthFromDate = function (pField) -{ - switch (this.dbType) - { - case db.DBTYPE_ORACLE10_CLUSTER: - case db.DBTYPE_ORACLE10_THIN: - case db.DBTYPE_ORACLE10_OCI: - return "to_char(" + pField + ",'MM')"; - case db.DBTYPE_DERBY10: - case db.DBTYPE_SQLSERVER2000: - case db.DBTYPE_MYSQL4: - case db.DBTYPE_MARIADB10: - return "month(" + pField + ")"; - case db.DBTYPE_POSTGRESQL8: - return "extract (month from " + pField + ")"; - } -} - -/** - * gets the year from a timestamp - * - * @param {String} pField timestamp to get the year from - * - * @return {String} sql expression that extracts the year from a timestamp - */ -SqlMaskingUtils.prototype.yearFromDate = function(pField) -{ - switch (this.dbType) - { - case db.DBTYPE_ORACLE10_CLUSTER: - case db.DBTYPE_ORACLE10_THIN: - case db.DBTYPE_ORACLE10_OCI: - return "to_char(" + pField + ",'yyyy')"; - case db.DBTYPE_DERBY10: - case db.DBTYPE_SQLSERVER2000: - case db.DBTYPE_MYSQL4: - case db.DBTYPE_MARIADB10: - return "YEAR(" + pField + ")"; - case db.DBTYPE_POSTGRESQL8: - return "EXTRACT (YEAR FROM " + pField + ")"; - } -} - - -/** - * gets the hour from a timestamp - * - * @param {String} pField timestamp to get the year from - * - * @return {String} sql expression that extracts the hour from a timestamp - */ -SqlMaskingUtils.prototype.hourFromDate = function(pField) -{ - switch (this.dbType) - { - case db.DBTYPE_ORACLE10_CLUSTER: - case db.DBTYPE_ORACLE10_THIN: - case db.DBTYPE_ORACLE10_OCI: - return "to_char(" + pField + ",'HH24')"; - case db.DBTYPE_DERBY10: - case db.DBTYPE_MYSQL4: - case db.DBTYPE_MARIADB10: - return "HOUR(" + pField + ")"; - case db.DBTYPE_SQLSERVER2000: - return "DATEPART(hour, "+ pField + ")"; - case db.DBTYPE_POSTGRESQL8: - return "EXTRACT (HOUR FROM " + pField + ")"; - } -} - - -/** - * gets the last day of the month from a timestamp - * - * @param {String} pField timestamp to get the last day of the month from - * - * @return {String} sql expression that extracts the last day of the month from a timestamp - */ -SqlMaskingUtils.prototype.lastDayOfMonth = function(pField) -{ - switch (this.dbType) - { - case db.DBTYPE_ORACLE10_CLUSTER: - case db.DBTYPE_ORACLE10_THIN: - case db.DBTYPE_ORACLE10_OCI: - case db.DBTYPE_DERBY10: - case db.DBTYPE_MYSQL4: - case db.DBTYPE_MARIADB10: - return "LAST_DAY(" + pField + ")"; - case db.DBTYPE_SQLSERVER2000: - return "DATEADD(MONTH, DATEDIFF(MONTH, 0 ," + pField + ") + 1, 0) -1"; - case db.DBTYPE_POSTGRESQL8: - return "DATE_TRUNC('month'," + pField + "::date) + interval '1 month' - interval '1 day')::date"; - } -} - - -/** - * returns the first field, that is not null or empty - * masks the behaviour of coalesce with case when - * - * @param pFields {Array} Array of fieldnames. Has to be in the right order. It will be checked from 0 upwards - */ -SqlMaskingUtils.prototype.coalesce = function(pFields) -{ - var retSql = ""; - - if(pFields && typeof pFields == "object" && pFields.length) - { - retSql = "case "; - - for(let i = 0; i < pFields.length; i++) - { - retSql += " when (" + pFields[i] + " is not null or " + pFields[i] + " <> '') then " + pFields[i] + " " - } - - retSql += " else null end"; - } - else - throw {message:"The input to coalesce has to be an Array containing the column names"}; - - return retSql; -} - -/** - * Converts day, month and year fields into a date - * The parameters can't be null!!! - * - * @param pYear {String} year field - * @param pMonth {String} month field - * @param pDay {String} day field - */ -SqlMaskingUtils.prototype.makeDate = function(pYear, pMonth, pDay) -{ - switch(this.dbType) - { - case db.DBTYPE_POSTGRESQL8: - return "make_date" + "(" + pYear + ", " + pMonth + ", " + pDay + ")"; - case db.DBTYPE_SQLSERVER2000: - return "datefromparts" + "(" + pYear + ", " + pMonth + ", " + pDay + ")"; - - case db.DBTYPE_ORACLE10_CLUSTER: - case db.DBTYPE_ORACLE10_THIN: - case db.DBTYPE_ORACLE10_OCI: - return "to_date(lpad(" + pYear + ", 4, '0') || lpad(" + pMonth + ", 2, '0') || lpad(" + pYear + ", 2, '0'), 'YYYY-MM-DD')"; - - // from datestr without filled zeros e.g.: 2000-5-2 - case db.DBTYPE_MYSQL4: - case db.DBTYPE_MARIADB10: - var dateFields = [ - this.cast(pYear, SQLTYPES.CHAR, 4), - this.cast(pMonth, SQLTYPES.CHAR, 2), - this.cast(pDay, SQLTYPES.CHAR, 2) - ]; - var dateStr = this.concatWithSeparator(dateFields, "-", false); - return this.cast(dateStr, SQLTYPES.DATE); - - // from datestr with filled zeros e.g.: 2000-05-02 - case db.DBTYPE_DERBY10: - // maskingUtils: workaround - function _castWithZeros(maskingUtils, field, size) { - var casted = maskingUtils.cast(field, SQLTYPES.VARCHAR, size); - var concatenated = maskingUtils.concatenate(["'000'", casted]); - // + 1: derbys substr starts counting at 1, for whatever reason - return "substr(" + concatenated + ", length(" + concatenated + ") - " + size + " + 1)"; - } - var dateFieldsZeroed = [ - _castWithZeros(this, pYear, 4), - _castWithZeros(this, pMonth, 2), - _castWithZeros(this, pDay, 2) - ]; - var dateStrZeroed = this.concatWithSeparator(dateFieldsZeroed, "-", false); - return this.cast(dateStrZeroed, SQLTYPES.DATE); - } -} - -/** - * Returns the database equivalent of the current date function as String depending on the database type - * - */ -SqlMaskingUtils.prototype.currentDate = function() -{ - switch(this.dbType) - { - case db.DBTYPE_POSTGRESQL8: - case db.DBTYPE_ORACLE10_CLUSTER: - case db.DBTYPE_ORACLE10_THIN: - case db.DBTYPE_ORACLE10_OCI: - case db.DBTYPE_MARIADB10: - case db.DBTYPE_DERBY10: - return "current_date"; - case db.DBTYPE_MYSQL4: - return "current_date()"; - case db.DBTYPE_SQLSERVER2000: - return "getdate()"; - } -} - -/** - * Returns the database equivalent of the fetch first row only function as String depending on the database type - * - */ -SqlMaskingUtils.prototype.limit = function(pRowAmount) -{ - switch(this.dbType) - { - case db.DBTYPE_POSTGRESQL8: - case db.DBTYPE_MARIADB10: - case db.DBTYPE_MYSQL4: - return "LIMIT " + pRowAmount; - case db.DBTYPE_SQLSERVER2000: - return "offset 0 rows fetch first " + pRowAmount + " rows only"; - case db.DBTYPE_DERBY10: - case db.DBTYPE_ORACLE10_CLUSTER: - case db.DBTYPE_ORACLE10_THIN: - case db.DBTYPE_ORACLE10_OCI: - return "fetch first " + pRowAmount + " rows only"; - } -} - -/** - * functions for various Sql-actions - * Do not create an instance of this! - * - * @class - * @static - */ -function SqlUtils() {} +function SqlUtils() {} /** * parses given name of table and name of column to clearly find out the tablename and columnanme @@ -4385,11 +3751,31 @@ SqlUtils._parseFieldQualifier = function(pFieldOrTableName, pColumnName) SqlUtils.isFullFieldQualifier = function(pFieldOrTableName, pColumnName) { var parsed = SqlUtils._parseFieldQualifier(pFieldOrTableName, pColumnName); - if (parsed instanceof TypeError) - return false; - return true; + return !(parsed instanceof TypeError); }; +/** + * Checks if the given value has the correct format for a prepared sql array + * + * @param {Object|Array} pPreparedArray value to check + * @return {Boolean} if the format is correct + */ +SqlUtils.isPreparedSqlArray = function (pPreparedArray) +{ + return pPreparedArray.length === 2 && Utils.isString(pPreparedArray[0]) && Array.isArray(pPreparedArray[1]); +} + +/** + * Checks if the given value has the correct format for a prepared sql array and contains something + * + * @param {Object|Array} pPreparedArray value to check + * @return {Boolean} if the format is correct + */ +SqlUtils.isNonEmptyPreparedSqlArray = function (pPreparedArray) +{ + return SqlUtils.isPreparedSqlArray(pPreparedArray) && pPreparedArray[0] != ""; +} + /** * determines the type of a single database column in a table; if you want to get several columntypes at once use db.getColumnTypes instead * @@ -4539,313 +3925,980 @@ SqlUtils._pageData = function(sqlType ,sqlStatement, blockSize, callbackFn, dbAl else if (data.length < blockSize || blockSize == 0)//blocksize 0 is everything startOffset = -1;//call callback the last time - if (callbackFn.call(this, data, ++count) === false) - return false;//callback can return false to manually stop the paging-process + if (callbackFn.call(this, data, ++count) === false) + return false;//callback can return false to manually stop the paging-process + } + return true; +} + +/** + * @return the alias for table asys_binaries + */ +SqlUtils.getBinariesAlias = function() +{ + return SqlUtils.getSystemAlias(); +} + +/** + * @return the sytemalias + */ +SqlUtils.getSystemAlias = function() +{ + return "_____SYSTEMALIAS"; +} + +/** + * @return the dataalias + */ +SqlUtils.getDataAlias = function() +{ + return "Data_alias"; +} + +/** + * Builds a SQL IN condition, while accounting for the 1000 elements maximum + * Single conditions are concatenated with OR, which can be devastating for performance! + * + * @param {String} pFieldname req name of the field with table alias + * z.B ORGREL.CONTACTID + * @param {String[]|String[][]} pData req Data as ID Array + * @param {String} [pQuoteSymbol=""] symbol for quoting values, + * Strings i.e.: ' default is no symbol + * @param {Boolean} [pAsPrepared=undefined] true if result should be returned as prepared condition + * @param {Boolean} [pPreparedDbType=undefined] if pAsPrepared is true, this param has to be filld with the correct db type + * + * @return {String|Array} SQL condition: where VALS in (1,2,3) OR as prepared Statement if pAsPrepared is true ["VALS in (1,2,3)", [...] + */ +SqlUtils.getSqlInStatement = function(pFieldname, pData, pQuoteSymbol, pAsPrepared, pPreparedDbType) +{ + var MAX_COUNT = 1000; + if (pData.length > 1000) + logging.log(translate.text("SqlUtils.getSqlInStatement: WARNING: You should not create in-statements with more than 1000 values. As this has a very bad performance.")) + + if (pData.length == 0) + return " 1 = 2 "; + + var res = ""; + var qs = pQuoteSymbol || ""; + + var preparedValues; + if (pAsPrepared) + { + preparedValues = []; + if (!pPreparedDbType) + throw new Error(translate.text("SqlUtils.getSqlInStatement: if pAsPrepared is true, pPreparedDbType has to be filld with the correct db type")); + } + + //pData.length -1 um für den Fall, dass MAX_COUNT == pData.length ist trotzdem nur einen Aufruf + //zu machen + var count = ((pData.length -1) / MAX_COUNT) >> 0;//aus kommazahl eine ganzzahl machen + //<= verwenden, da bei einer Länge von "126" der Vorgang einmal ausgeführt werden soll + for (var i = 0; i <= count; i++) + { + if (i > 0) + res += " or "; + + if (pAsPrepared) + { + res += (pFieldname ? pFieldname + " in " : "") + "("; + var subData = pData.slice(i * MAX_COUNT, i * MAX_COUNT + MAX_COUNT); + + subData.forEach(function(pVal, pIndex) { + res += "?"; + preparedValues.push([pVal, pPreparedDbType]) + if (pIndex != subData.length-1) + res += ", "; + }); + res += ")" + } + else + { + res += (pFieldname ? pFieldname + " in " : "") + "(" + qs + pData.slice(i * MAX_COUNT, i * MAX_COUNT + MAX_COUNT) + .join(qs + ", " + qs) + qs + ") "; + } + } + + //wenn mehrere Zeilen mit "or" verknüpft wurden nochmal klammern + if (count > 0) + res = "(" + res + ")"; + + if (pAsPrepared) + return [res, preparedValues]; + else + return res; +} + +/** +* resolves key-value pairs (of strings) into a case when expression; +* This function tries to get the columntype for better type comparison +* +* @param {String[][]} pKeyValueArray you've to pass a 2D-Array where each element has at pos0 the key and pos1 the value +* @param {String} pDbFieldName name fo the database field where the KEY-value is stored; prefers TABLENAME.COLUMNNAME +* @param {String} [pLocale=current client language] specifies the locale for translating the title; can be false if nothing shalle be translated +* +* @return {String} a SQL-expression (case-when-statement) that resolves the KEYID into the title -> as preparedSatement-elements +*/ +SqlUtils.getResolvingCaseWhen = function(pKeyValueArray, pDbFieldName, pLocale) +{ + var keyData = pKeyValueArray; + if (keyData.length == 0) + return ["''", []]; + + //a helper function for easy translation + var translateValue = function(value){ + if (pLocale === false) + return value; + else if (pLocale) + return translate.text(value, pLocale); + else + return translate.text(value); + }; + //!SqlBuilder + var resSql = "case ", preparedValues = []; + + var colTypeKeyId = SQLTYPES.CHAR; //the standard type is char + var fields = SqlUtils._parseFieldQualifier(pDbFieldName); //validate the DB-field for proper form (CONTACT.CONTACTID) + if (!(fields instanceof TypeError)) + colTypeKeyId = SqlUtils.getSingleColumnType(pDbFieldName, undefined, this.alias); + //some databases dont auto cast on their own so we need the proper type + + var colTypeTitle = SQLTYPES.NVARCHAR; + for (var i = 0, l = keyData.length; i < l; i++) + { + var translatedTitle = translateValue(keyData[i][1]); + resSql += " when " + pDbFieldName + " = ? then ? " + preparedValues.push([keyData[i][0], colTypeKeyId]); + preparedValues.push([translatedTitle, colTypeTitle]); + } + resSql += " else '' end "; + resSql = [resSql, preparedValues]; + return resSql; +}; + +/** +* resolves an array of key-value pairs (of strings) into a sql case when expression<br/> +* This is useful for results of entities.getRows for example. +* +* @param {Array} pKeyValueObject <p/>you've to pass a 2D-Array where each element has to be an object with at least one key: value-pair, e.g.: +* <br/>[{uid: "uid1", value: "value1"}, {uid: "uidN", value: "valueN"}] +* @param {String} pUid <p/>name of the key where the rawvalue (the uid) is located in the object +* @param {String} pTranslatedValue <p/>name of the key where the already translated value is located in the object +* @param {String} pDbFieldName <p/>name fo the database field where the KEY-value is stored +* +* @return {String} <p/>a SQL-expression (case-when-statement) that resolves the KEYID into the title -> as +* preparedSatement-elements +* <br/>The else-value is "unassigned". +* +* @example +* var exampleDataStack = [ +* {keyVal: "PHONE", titleOriginal: "Phone", titleTranslated: "Telefon", origin: "MetaImporter"}, +* {keyVal: "EMAIL", titleOriginal: "email", titleTranslated: "E-Mail", origin: "MetaImporter"} +* ]; +* +* var sqlExpr = SqlUtils.getResolvingCaseWhenFromObject(exampleDataStack, "keyVal", "titleTranslated", "FORM.COMMUNICATION"); +* //results in a sql case when as prepared statement that is resolvedas following: +* //case when FORM.COMMUNICATION = 'PHONE' then 'Telefon' when FORM.COMMUNICATION = 'EMAIL' then 'E-Mail' else 'nicht zugeordnet' end +*/ +SqlUtils.getResolvingCaseWhenFromObject = function(pKeyValueObject, pUid, pTranslatedValue, pDbFieldName) +{ + var keyData = pKeyValueObject; + if (keyData.length == 0) + return ["''", []]; + + var translateValue = pTranslatedValue; + var uid = pUid; + var unassigned = translate.text("unassigned") + + var resSql = "case ", preparedValues = []; + var colTypeKeyId = SQLTYPES.CHAR; + var colTypeTitle = SQLTYPES.NVARCHAR; + for (var i = 0, l = keyData.length; i < l; i++) + { + var translatedTitle = keyData[i][translateValue]; + resSql += " when " + pDbFieldName + " = ? then ? " + preparedValues.push([keyData[i][pUid], colTypeKeyId]); + preparedValues.push([translatedTitle, colTypeTitle]); + + } + resSql += " else '"+ unassigned +"' end "; + resSql = [resSql, preparedValues]; + return resSql; +}; + +/** + * Will quote all prepared statement values from the given statement. + * + * @param {PreparedSqlArray} pStatement Same as first paraemter of db.translateStatement. + * @param {String} pAlias The database alias + * @param {Callback} pExecutionCallback (PreparedSqlArray) => String A function which must return the final SQL. + * @return The SQL, same as the result of db.translateStatement. + */ +SqlUtils.translateWithQuotes = function(pStatement, pAlias, pExecutionCallback) +{ + // Validate type of incoming paramter. + if (!Array.isArray(pStatement)) + return null; + + // The second element of the array has to be an array. + if (!Array.isArray(pStatement[1])) + return null; + + // As the second element represents the prepared statements we need to map it... + var preparedStatements = pStatement[1].map(function(pValue) + { + // Just in case as a fallback value.. + if (!(Array.isArray(pValue))) + return pValue; + + // As the first element represents the value it will be quoted here. + if(pAlias) + { + return [db.quote(pValue[0], pAlias), pValue[1]]; + } + else + { + return [db.quote(pValue[0]), pValue[1]]; + } + }); + + return pExecutionCallback([pStatement[0], preparedStatements]); +} + +/** + * Will quote all prepared statement values from the given statement. + * + * @param {PreparedSqlArray} pStatement Same as the first parameter of db.translateStatement. + * @param {String} [pAlias] the alias which should be used for db.translateStatement() + * @returns {String} The SQL, same as the result of db.translateStatement. + */ +SqlUtils.translateStatementWithQuotes = function(pStatement, pAlias) +{ + return SqlUtils.translateWithQuotes(pStatement, pAlias, function(pValue) + { + if (pAlias) + return db.translateStatement(pValue, pAlias) + else + return db.translateStatement(pValue) + }); +} + +/** + * Will quote all prepared statement values from the given statement. + * + * @param {PreparedSqlArray} pStatement Same as the first parameter of db.translateCondition. + * @param {String} [pAlias] the alias which should be used for db.translateStatement() + * @returns {String} The SQL, same as the result of db.translateCondition. + */ +SqlUtils.translateConditionWithQuotes = function(pStatement, pAlias) +{ + return SqlUtils.translateWithQuotes(pStatement, pAlias, function(pValue) + { + if (pAlias) + return db.translateCondition(pValue, pAlias) + else + return db.translateCondition(pValue) + }); +} + +SqlUtils.parseField = function(pField) +{ + var alias = ""; + if (typeof pField === 'string') + { + var pointPos = pField.indexOf("."); + + if (pointPos > 0 && pointPos < pField.length-1) + alias = pField; + else + throw new Error(translate.text("${SQL_LIB_FIELD_WRONG_FORMAT}") + pField + translate.withArguments("${SQL_LIB_FIELD_WRONG_FORMAT} field: %0", [pField])); + } + else + { + if (pField.length == 2) + pField.push(pField[0]); + + if (pField.length == 3) + { + alias = pField[2] + "." + pField[1]; + pField = pField[0] + "." + pField[1]; + } + else + throw new Error(translate.text("${SQL_LIB_FIELD_WRONG_FORMAT}") + field.toSource() + translate.withArguments("${SQL_LIB_FIELD_WRONG_FORMAT} field: %0", [field.toSource()])); + } + return [alias, pField] +} + +SqlUtils.replaceConditionTemplate = function(pCondition, pPlaceholder, pReplacement) +{ + //SqlUtils.replaceConditionTemplate(pCondition, '#', SqlUtils.parseField(pFieldOrCond).join(".")) + + //this function looks more complex (and slower) than it actually is + /* the following regex looks like this after javascript-escaping of the backslash: (?<!\\)((?:\\\\)*)# + the regexp searches for the unescaped character and these characters are replaced by the field name + + examples: + --------------------- + | # --match | + | \# --no-match | + | \\# --match | + | \\\# --no-match | + | \\\\# --match | + --------------------- + */ + //use replaceAll because it's faster and supports negative lookbehinds + var replacements = {}; + //manually readd the replaced backslashes by using a group reference, because they a part of the match and therefore replaced by "replaceAll" + //since the field COULD contain already a group reference (I think this is extremely uncommon; + //probably that never happens but better stay save): escape that references within the fieldname + replacements["(?<!\\\\)((?:\\\\\\\\)*)" + pPlaceholder] = "$1" + text.replaceAll(pReplacement, { + "$1": "\\$1" + }) + //now that we've replaced the correct field placeholder let's replace the escaped placeholder sign "\#" to a normal placeholder sign "#" + replacements["\\\\" + pPlaceholder] = pPlaceholder + + return text.replaceAll(pCondition, replacements); +} + +/** + * Checks if the '#' is 0 or 1 time in pCondition, '?' has to be 1 time in pCondition. + * Also checks if '#' is before '?' + * @param {String} pCondition + * + * @return {Boolean} true if the format is ok + */ +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 (it uses text.replaceAll which supports lookbehind because it uses java) + 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@}")) +} + +/** + * Escapes a jdito variable for the value of a SqlBuilder condition. SqlBuilder.prototype.where/and/or/... automatically resolve the value as a jdito + * variable if it starts with a single '$', so you can use this function to make sure the value is used as it is. + * <br> + * Note: The main purpose of this is to prevent errors resulting from unexpected user inputs. But if you are loading the input from a jdito variable + * anyways, you can just wite the variable name as the condition value and it will be safe. + * + * @param {String} pValue the value + * @return {String} the escaped string + * @example + * + * var sqlCond = newWhere("TABLE.COLUMN", SqlUtils.escapeVars(userInput)); //userInput could start with '$' + */ +SqlUtils.escapeVars = function (pValue) +{ + if (typeof(pValue) == "string" && pValue.charAt(0) == "$") + return "$" + pValue; + return pValue; +} + +SqlUtils.getSqlConditionalOperator = function(pOperator) +{ + switch(parseInt(pOperator)) + { + case 1: + return SqlBuilder.EQUAL(); + case 2: + return SqlBuilder.NOT_EQUAL(); + + case 4: + return SqlBuilder.LESS(); + case 5: + return SqlBuilder.LESS_OR_EQUAL(); + + case 3: + return SqlBuilder.GREATER(); + case 6: + return SqlBuilder.GREATER_OR_EQUAL(); + + case 11: + return "# is not null"; + case 12: + return "# is null"; + + default: + throw new Error("Unsupported operator " + pOperator); + } +} + +/** + * Returns the pNullableExpr if pNullableExpr is not null + * otherwise it returns pDefaultExpr + * + * @param {string|SqlBuilder} pNullableExpr a nullable expression + * @param {string|SqlBuilder} pDefaultExpr the default expression if pNullableExpr is null + * + * @returns {SqlBuilder._CaseStatement} pNullableExpr with pDefaultExpr as fallback + */ +SqlUtils.nullableWithDefault = function(pNullableExpr, pDefaultExpr) +{ + return SqlBuilder.caseStatement() + .when("(" + pNullableExpr.toString() + ") is null").then(pDefaultExpr) + .elseValue(pNullableExpr); +} + + + + + + + + + +/** + * object for easier handling of conditions; + * With this object you do not have to check if the string is empty or not; + * you don't need to append a "1=1" condition or similar; + * this objects gains most benefit if you have a lot of conditions that are added (or not) depending on tons of JDito-conditions + * + * You can also use SqlCondition.begin(alias) for simpler object creation without new and without the need for an extra variable to save the object. + * + * @class + * @param {String} [alias=the current alias] the database alias where the condition shall be executed later (important for column types of preparedStatements) + * @example + * see others/guide/HowToSqlConditionLib.adoc + * + * @deprecated The SqlCondition will be removed in version >= 2020.x + * Use the SqlBuilder instead. + * For SqlBuilder usage see the documentation-property of the Sql_lib. + */ +function SqlCondition(alias) { + // setting null is only needed to provide autocomplete for the ADITO-designer + this.preparedValues = null; + this._init(); // the properties are initalized in an extra function because init is nearly the same as resetting (clearing) the SqlConditions + this.alias = alias; + + // save, if the last condition was an OR. For better bracket-placement + this._lastWasOr = false; +} + +/** + * Alternative possibility to crate a new condition. + * With this you don't need new SqlCondition and you can use the object directly after it's creation + * --> cleaner code + * + * It is very usefull for the orSqlCondition() and andSqlCondition() because now you can create a new condition inline. + * You can also use it for simple selects without the need to save the conditionObject in an extra variable. + * See Examples! + * + * @param {String} [alias=the current alias] the database alias where the condition shall be executed later (important for column types of preparedStatements) + * @return {SqlCondition} the new SqlCondition-object + * + * @example + * vars mySelect = SqlCondition.begin(alias) + * .and("MYID = '123'") + * .and(SqlCondition.begin() + * .or("NAME = 'Max'") + * .or("NAME = 'Bob'") + * ) + * .buildSql("select * from MYTABLE"); + * + * // Or use it for simple selects: + * var sum = db.cell(SqlCondition.begin() + * .andPrepared("STOCK.PRODUCT_ID", pid) + * .buildSql("select sum(QUANTITY * IN_OUT) from STOCK")); + * + * @deprecated The SqlCondition will be removed in version >= 2020.x + * Use the SqlBuilder instead. + * For SqlBuilder usage see the documentation-property of the Sql_lib." + */ +SqlCondition.begin = function(alias) { + return new SqlCondition(alias); +} + +/** + * checks if conditions have been added to the object + * @return {Boolean} true if conditions have been added, false when not + */ +SqlCondition.prototype.isSet = function() { + if (this._sqlStorage) + return true; + return false; +} + + +/** + * append with SQL-and; no paranthesize of existing conditions is done + * @param {String} cond the condition string which shall be appended + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.and = function(cond) { + if (!cond) + return this; + if (this.isSet()) + this._sqlStorage += " and "; + this._sqlStorage += cond; + return this; +} + +/** + * append with SQL-or; Also paranthesize the existing conditions + * @param {String} cond the condition string which shall be appended + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.or = function(cond) { + if (!cond) + return this; + + if (this.isSet() && !this._lastWasOr) { + this._sqlStorage = "(" + this._sqlStorage + ") or (" + cond + ")"; + this._lastWasOr = true; + + } else if (this.isSet() && this._lastWasOr) { + this._sqlStorage = this._sqlStorage + " or (" + cond + ")"; + this._lastWasOr = true; + + } else { + this._sqlStorage = cond; + } + return this; +} + +/** + * append a prepared-array to this sql condition with SQL-and + * @param {Array} preparedObj a prepared condition-array + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.andAttachPrepared = function(preparedObj) { + if (preparedObj) + { + this.preparedValues = this.preparedValues.concat(preparedObj[1]); + return this.and(preparedObj[0]); } - return true; + + return this; } /** - * @return the alias for table asys_binaries + * append a prepared-array to this sql condition with SQL-or + * @param {Array} preparedObj a prepared condition-array + * @return {SqlCondition} current SqlCondition-object */ -SqlUtils.getBinariesAlias = function() -{ - return SqlUtils.getSystemAlias(); +SqlCondition.prototype.orAttachPrepared = function(preparedObj) { + if (preparedObj) + { + this.preparedValues = this.preparedValues.concat(preparedObj[1]); + return this.or(preparedObj[0]); + } + + return this; } /** - * @return the sytemalias + * append another condition with SQL-and + * + * @param {SqlCondition} cond the condition which shall be appended + * @param {String} [alternativeCond=""] condition if the given SqlCondition has none + * @return {SqlCondition} current SqlCondition-object */ -SqlUtils.getSystemAlias = function() -{ - return "_____SYSTEMALIAS"; +SqlCondition.prototype.andSqlCondition = function(cond, alternativeCond) { + if (!cond) + return this + + var otherCondition = cond.toString(alternativeCond); + if (otherCondition.trim() != "") + { + this.and(" ( " + cond.toString(alternativeCond) + " ) "); + if (cond.preparedValues) { + this.preparedValues = this.preparedValues.concat(cond.preparedValues); + } + } + return this; } /** - * @return the dataalias + * append another condition with SQL-or; Also paranthesize the existing conditions + * + * @param {SqlCondition} cond the condition which shall be appended + * @param {String} [alternativeCond=""] condition if the given SqlCondition has none + * @return {SqlCondition} current SqlCondition-object */ -SqlUtils.getDataAlias = function() -{ - return "Data_alias"; +SqlCondition.prototype.orSqlCondition = function(cond, alternativeCond) { + var otherCondition = cond.toString(alternativeCond); + if (otherCondition.trim() != "") + { + this.or(" ( " + cond.toString(alternativeCond) + " ) "); + if (cond.preparedValues) { + this.preparedValues = this.preparedValues.concat(cond.preparedValues); + } + } + return this; } /** - * Builds a SQL IN condition, while accounting for the 1000 elements maximum - * Single conditions are concatenated with OR, which can be devastating for performance! - * - * @param {String} pFieldname req name of the field with table alias - * z.B ORGREL.CONTACTID - * @param {String[]|String[][]} pData req Data as ID Array - * @param {String} [pQuoteSymbol=""] symbol for quoting values, - * Strings i.e.: ' default is no symbol - * @param {Boolean} [pAsPrepared=undefined] true if result should be returned as prepared condition - * @param {Boolean} [pPreparedDbType=undefined] if pAsPrepared is true, this param has to be filld with the correct db type - * - * @return {String|Array} SQL condition: where VALS in (1,2,3) OR as prepared Statement if pAsPrepared is true ["VALS in (1,2,3)", [...] - */ -SqlUtils.getSqlInStatement = function(pFieldname, pData, pQuoteSymbol, pAsPrepared, pPreparedDbType) -{ - var MAX_COUNT = 1000; - if (pData.length > 1000) - logging.log(translate.text("SqlUtils.getSqlInStatement: WARNING: You should not create in-statements with more than 1000 values. As this has a very bad performance.")) + * append an condition that uses a subQuery with SQL-and + * + * @param {SqlBuilder} subQuery the SqlBuilder object that will be used as a subquery + * @param {String} [cond="exists"] condition that is used (e. g. exists, not exists, COLUMN = any, COLUMN in, ...) + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.andSqlBuilder = function(subQuery, cond) { + if (!cond) + cond = "exists"; - if (pData.length == 0) - return " 1 = 2 "; + var preparedObj = subQuery.build(); + preparedObj[0] = cond + " ( " + preparedObj[0] + " ) "; + this.andAttachPrepared(preparedObj); + + return this; +} - var res = ""; - var qs = pQuoteSymbol || ""; +/** + * append an condition that uses a subQuery with SQL-or + * + * @param {SqlBuilder} subQuery the SqlBuilder object that will be used as a subquery + * @param {String} [cond="exists"] condition that is used (e. g. exists, not exists, COLUMN = any, COLUMN in, ...) + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.orSqlBuilder = function(subQuery, cond) { + if (!cond) + cond = "exists"; - var preparedValues; - if (pAsPrepared) - { - preparedValues = []; - if (!pPreparedDbType) - throw new Error(translate.text("SqlUtils.getSqlInStatement: if pAsPrepared is true, pPreparedDbType has to be filld with the correct db type")); - } + var preparedObj = subQuery.build(); + preparedObj[0] = cond + " ( " + preparedObj[0] + " ) "; + this.orAttachPrepared(preparedObj); + + return this; +} - //pData.length -1 um für den Fall, dass MAX_COUNT == pData.length ist trotzdem nur einen Aufruf - //zu machen - var count = ((pData.length -1) / MAX_COUNT) >> 0;//aus kommazahl eine ganzzahl machen - //<= verwenden, da bei einer Länge von "126" der Vorgang einmal ausgeführt werden soll - for (var i = 0; i <= count; i++) - { - if (i > 0) - res += " or "; - - if (pAsPrepared) - { - res += (pFieldname ? pFieldname + " in " : "") + "("; - var subData = pData.slice(i * MAX_COUNT, i * MAX_COUNT + MAX_COUNT); - - subData.forEach(function(pVal, pIndex) { - res += "?"; - preparedValues.push([pVal, pPreparedDbType]) - if (pIndex != subData.length-1) - res += ", "; - }); - res += ")" - } - else - { - res += (pFieldname ? pFieldname + " in " : "") + "(" + qs + pData.slice(i * MAX_COUNT, i * MAX_COUNT + MAX_COUNT) - .join(qs + ", " + qs) + qs + ") "; - } - } +/** + * same as the "and"-function but with preparedStatement functionality + * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] + * @param {String} value the value that shall be set into the prepared statement + * @param {String} [cond="# = ?"] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; + * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" + * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; + * The loaded type is cached if no type is given. So it is also safe to use this in a loop. + * e.g. + * for (...) { + * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") + * } + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.andPrepare = function(field, value, cond, fieldType) { + cond = this._prepare(field, value, cond, fieldType); + return this.and(cond); +} - //wenn mehrere Zeilen mit "or" verknüpft wurden nochmal klammern - if (count > 0) - res = "(" + res + ")"; +/** + * same as the "or"-function but with preparedStatement functionality + * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] + * @param {String} value the value that shall be set into the prepared statement + * @param {String} [cond="# = ?"] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; + * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" + * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; + * The loaded type is cached if no type is given. So it is also safe to use this in a loop. + * e.g. + * for (...) { + * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") + * } + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.orPrepare = function(field, value, cond, fieldType) { + cond = this._prepare(field, value, cond, fieldType); + return this.or(cond); +} - if (pAsPrepared) - return [res, preparedValues]; - else - return res; +/** + * same as the "andPrepare"-function but only applied if the passed "value" is truely + * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] + * @param {String} value the value that shall be set into the prepared statement + * @param {String} [cond="# = ?"] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; + * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" + * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; + * The loaded type is cached if no type is given. So it is also safe to use this in a loop. + * e.g. + * for (...) { + * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") + * } + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.andPrepareIfSet = function(field, value, cond, fieldType) { + if (value) + return this.andPrepare(field, value, cond, fieldType); + return this; +} + +/** + * same as the "orPrepare"-function but only applied if the passed "value" is truely + * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] + * @param {String} value the value that shall be set into the prepared statement + * @param {String} [cond="# = ?"] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; + * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" + * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; + * The loaded type is cached if no type is given. So it is also safe to use this in a loop. + * e.g. + * for (...) { + * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") + * } + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.orPrepareIfSet = function(field, value, cond, fieldType) { + if (value) + return this.orPrepare(field, value, cond, fieldType); + return this; } /** -* resolves key-value pairs (of strings) into a case when expression; -* This function tries to get the columntype for better type comparison -* -* @param {String[][]} pKeyValueArray you've to pass a 2D-Array where each element has at pos0 the key and pos1 the value -* @param {String} pDbFieldName name fo the database field where the KEY-value is stored; prefers TABLENAME.COLUMNNAME -* @param {String} [pLocale=current client language] specifies the locale for translating the title; can be false if nothing shalle be translated -* -* @return {String} a SQL-expression (case-when-statement) that resolves the KEYID into the title -> as preparedSatement-elements -*/ -SqlUtils.getResolvingCaseWhen = function(pKeyValueArray, pDbFieldName, pLocale) -{ - var keyData = pKeyValueArray; - if (keyData.length == 0) - return ["''", []]; - - //a helper function for easy translation - var translateValue = function(value){ - if (pLocale === false) - return value; - else if (pLocale) - return translate.text(value, pLocale); - else - return translate.text(value); - }; - //!SqlBuilder - var resSql = "case ", preparedValues = []; - - var colTypeKeyId = SQLTYPES.CHAR; //the standard type is char - var fields = SqlUtils._parseFieldQualifier(pDbFieldName); //validate the DB-field for proper form (CONTACT.CONTACTID) - if (!(fields instanceof TypeError)) - colTypeKeyId = SqlUtils.getSingleColumnType(pDbFieldName, undefined, this.alias); - //some databases dont auto cast on their own so we need the proper type - - var colTypeTitle = SQLTYPES.NVARCHAR; - for (var i = 0, l = keyData.length; i < l; i++) - { - var translatedTitle = translateValue(keyData[i][1]); - resSql += " when " + pDbFieldName + " = ? then ? " - preparedValues.push([keyData[i][0], colTypeKeyId]); - preparedValues.push([translatedTitle, colTypeTitle]); + * same as the "andPrepare"-function but with validation of adito-variables functionality + * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] + * @param {String} variable the adito-variable that shall be set into the prepared statement + * @param {String} [cond = "# = ?" ] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; + * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" + * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; + * The loaded type is cached if no type is given. So it is also safe to use this in a loop. + * e.g. + * for (...) { + * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") + * } + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.andPrepareVars = function(field, variable, cond, fieldType) { + variable = this._checkVars(variable) + if (variable) { + return this.andPrepare(field, variable, cond, fieldType); } - resSql += " else '' end "; - resSql = [resSql, preparedValues]; - return resSql; -}; + return this; +} /** -* resolves an array of key-value pairs (of strings) into a sql case when expression<br/> -* This is useful for results of entities.getRows for example. -* -* @param {Array} pKeyValueObject <p/>you've to pass a 2D-Array where each element has to be an object with at least one key: value-pair, e.g.: -* <br/>[{uid: "uid1", value: "value1"}, {uid: "uidN", value: "valueN"}] -* @param {String} pUid <p/>name of the key where the rawvalue (the uid) is located in the object -* @param {String} pTranslatedValue <p/>name of the key where the already translated value is located in the object -* @param {String} pDbFieldName <p/>name fo the database field where the KEY-value is stored -* -* @return {String} <p/>a SQL-expression (case-when-statement) that resolves the KEYID into the title -> as -* preparedSatement-elements -* <br/>The else-value is "unassigned". -* -* @example -* var exampleDataStack = [ -* {keyVal: "PHONE", titleOriginal: "Phone", titleTranslated: "Telefon", origin: "MetaImporter"}, -* {keyVal: "EMAIL", titleOriginal: "email", titleTranslated: "E-Mail", origin: "MetaImporter"} -* ]; -* -* var sqlExpr = SqlUtils.getResolvingCaseWhenFromObject(exampleDataStack, "keyVal", "titleTranslated", "FORM.COMMUNICATION"); -* //results in a sql case when as prepared statement that is resolvedas following: -* //case when FORM.COMMUNICATION = 'PHONE' then 'Telefon' when FORM.COMMUNICATION = 'EMAIL' then 'E-Mail' else 'nicht zugeordnet' end -*/ -SqlUtils.getResolvingCaseWhenFromObject = function(pKeyValueObject, pUid, pTranslatedValue, pDbFieldName) -{ - var keyData = pKeyValueObject; - if (keyData.length == 0) - return ["''", []]; - - var translateValue = pTranslatedValue; - var uid = pUid; - var unassigned = translate.text("unassigned") - - var resSql = "case ", preparedValues = []; - var colTypeKeyId = SQLTYPES.CHAR; - var colTypeTitle = SQLTYPES.NVARCHAR; - for (var i = 0, l = keyData.length; i < l; i++) - { - var translatedTitle = keyData[i][translateValue]; - resSql += " when " + pDbFieldName + " = ? then ? " - preparedValues.push([keyData[i][pUid], colTypeKeyId]); - preparedValues.push([translatedTitle, colTypeTitle]); - + * same as the "orPrepare"-function but with validation of adito-variables functionality + * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] + * @param {String} variable the adito-variable that shall be set into the prepared statement + * @param {String} [cond="# = ?"] the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; + * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" + * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; + * The loaded type is cached if no type is given. So it is also safe to use this in a loop. + * e.g. + * for (...) { + * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") + * } + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.orPrepareVars = function(field, variable, cond, fieldType) { + variable = this._checkVars(variable) + if (variable) { + return this.orPrepare(field, variable, cond, fieldType); } - resSql += " else '"+ unassigned +"' end "; - resSql = [resSql, preparedValues]; - return resSql; -}; + return this; +} /** - * Will quote all prepared statement values from the given statement. + * creates a IN-statement out of a field and an array of values. + * Be carefull with a big number of values. This may have a bad performance. * - * @param {PreparedSqlArray} pStatement Same as first paraemter of db.translateStatement. - * @param {String} pAlias The database alias - * @param {Callback} pExecutionCallback (PreparedSqlArray) => String A function which must return the final SQL. - * @return The SQL, same as the result of db.translateStatement. + * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] + * @param {String[]} values the value that shall be set into the prepared statement + * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; + * The loaded type is cached if no type is given. So it is also safe to use this in a loop. + * e.g. + * for (...) { + * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") + * } + * @param {Boolean} [not = undefined] if true, add not before in + * @return {SqlCondition} current SqlCondition-object */ -SqlUtils.translateWithQuotes = function(pStatement, pAlias, pExecutionCallback) -{ - // Validate type of incoming paramter. - if (!Array.isArray(pStatement)) - return null; +SqlCondition.prototype.andIn = function(field, values, fieldType, not) { + return this.andAttachPrepared(this._in(field, values, fieldType, not)); +} - // The second element of the array has to be an array. - if (!Array.isArray(pStatement[1])) - return null; +/** + * creates a IN-statement out of a field and an array of values. + * Be carefull with a big number of values. This may have a bad performance. + * + * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] + * @param {String[]} values the value that shall be set into the prepared statement + * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; + * The loaded type is cached if no type is given. So it is also safe to use this in a loop. + * e.g. + * for (...) { + * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") + * } + * @param {Boolean} [not = undefined] if true, add not before in + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype.orIn = function(field, values, fieldType, not) { + return this.orAttachPrepared(this._in(field, values, fieldType, not)); +} - // As the second element represents the prepared statements we need to map it... - var preparedStatements = pStatement[1].map(function(pValue) +/** + * creates a IN-statement out of a field and an array of values. + * Be carefull with a big number of values. This may have a bad performance. + * + * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] + * @param {String[]} values the value that shall be set into the prepared statement + * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; + * The loaded type is cached if no type is given. So it is also safe to use this in a loop. + * e.g. + * for (...) { + * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") + * } + * @param {Boolean} [not = undefined] if true, add not before in + * @return {SqlCondition} current SqlCondition-object + */ +SqlCondition.prototype._in = function(field, values, fieldType, not) { + if (values && values.length > 0) { - // Just in case as a fallback value.. - if (!(Array.isArray(pValue))) - return pValue; + if (fieldType == undefined) + fieldType = SqlUtils.getSingleColumnType(field, undefined, this.alias); + + preparedStatement = SqlUtils.getSqlInStatement(field, values, undefined, true, fieldType); + if (not) + preparedStatement[0] = " not " + preparedStatement[0]; + return preparedStatement; + } + + return null; +} - // As the first element represents the value it will be quoted here. - if(pAlias) - { - return [db.quote(pValue[0], pAlias), pValue[1]]; - } - else - { - return [db.quote(pValue[0]), pValue[1]]; - } - }); +/** + * ready to use string; does not contain a where keyword at the beginning + * @param {String} [alternativeCond=""] condition that is returned when nothing has been appended. + * @return {String} concatenated SQL-condition; empty string if nothing has been appended or - if passed - the alternativeCond + */ +SqlCondition.prototype.toString = function(alternativeCond) { + if (!this.isSet() && alternativeCond) + return alternativeCond + else + return this._sqlStorage; +} - return pExecutionCallback([pStatement[0], preparedStatements]); +/** + * ready to use string; does contain a where keyword at the beginning + * @param {String} [alternativeCond=""] condition that is returned when nothing has been appended. + * @return {SqlCondition} concatenated SQL-condition; empty string if nothing has been appended or - if passed - the alternativeCond + */ +SqlCondition.prototype.toWhereString = function(alternativeCond) { + var cond = this.toString(alternativeCond); + if (cond) + return " where " + cond; + else + return cond; +} + +/** + * ready to use prepared condition; does not contain a where keyword at the beginning + * @param {String} [alternativeCond=""] Condition that is returned when nothing has been appended. + * @return {Array[][][]} Prepared condition with [condition, [[field1, type1], [field2, type2]]] + */ +SqlCondition.prototype.build = function(alternativeCond) { + return [this.toString(alternativeCond), this.preparedValues]; +} + +/** + * ready to use prepared select + * @param {String} pBeforeCondition Part of the sql before the condition without where (e.g. "select FIRSTNAME from PERSON") + * @param {String} [pAlternativeCond=""] Condition that is returned when nothing has been appended. + * @param {String} [pAfterCondition=""] Part of the sql after the condition (e.g. "order by FIRSTNAME"). + * @param {Boolean} [pWithWere=true] true if where should be added to the bginning + * @return {Array[][][]} Prepared condition with [condition, [[field1, type1], [field2, type2]]] + */ +SqlCondition.prototype.buildSql = function(pBeforeCondition, pAlternativeCond, pAfterCondition, pWithWere) { + if (pAfterCondition == undefined) + pAfterCondition = ""; + + if (pWithWere == undefined) + pWithWere = true; + + return [pBeforeCondition + " " + + (pWithWere ? this.toWhereString(pAlternativeCond) : this.toString(pAlternativeCond)) + + " " + pAfterCondition, this.preparedValues]; } /** - * Will quote all prepared statement values from the given statement. - * - * @param {PreparedSqlArray} pStatement Same as the first parameter of db.translateStatement. - * @param {String} [pAlias] the alias which should be used for db.translateStatement() - * @returns {String} The SQL, same as the result of db.translateStatement. + * translates SqlCondition to plain SQL. Use this if prepared statements are not supported. + * It resolves all prepared values. + * @param {String} pAlternativeCond used if the SqlCondition does not contain any condition. + * @return {String} plain SQL condition */ -SqlUtils.translateStatementWithQuotes = function(pStatement, pAlias) +SqlCondition.prototype.translate = function(pAlternativeCond) { - return SqlUtils.translateWithQuotes(pStatement, pAlias, function(pValue) - { - if (pAlias) - return db.translateStatement(pValue, pAlias) - else - return db.translateStatement(pValue) - }); + return SqlUtils.translateConditionWithQuotes(this.build(pAlternativeCond, this.alias)); } /** - * Will quote all prepared statement values from the given statement. + * Check if (adito-)variable exists and vars.getString is not empty + * @param {String} variable the variable name (e.g. "$field.CONTACT_ID") + * @return {String | Boolean} The value of the field as string OR false if it doesn't exist. * - * @param {PreparedSqlArray} pStatement Same as the first parameter of db.translateCondition. - * @param {String} [pAlias] the alias which should be used for db.translateStatement() - * @returns {String} The SQL, same as the result of db.translateCondition. + * @ignore */ -SqlUtils.translateConditionWithQuotes = function(pStatement, pAlias) -{ - return SqlUtils.translateWithQuotes(pStatement, pAlias, function(pValue) - { - if (pAlias) - return db.translateCondition(pValue, pAlias) - else - return db.translateCondition(pValue) - }); +SqlCondition.prototype._checkVars = function(variable) { + if (vars.exists(variable)) { + var value = vars.getString(variable); + if (value) { + return value; + } + } + return false; } -SqlUtils.parseField = function(pField) -{ - var alias = ""; - if (typeof pField === 'string') +/** + * hidden function for composing preparedStatements + * @param {String | String[]} field the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" or as array with column-alias: ["ORGANISATION", "NAME", "myorgAlias"] + * @param {String} value the value that shall be set into the prepared statement + * @param {String} cond the strucutre of the SQL condition as preparedString, you can use a number sign "#" as placeholder for you fieldname; + * e.g. "# > ?"; escaping the number sign is possible with a backslash "\" + * Default is "# = ?" + * @param {Numeric | Boolean} [fieldType] SQL-column-type; if the fieldType is not given it's loaded automatically; + * The loaded type is cached if no type is given. So it is also safe to use this in a loop. + * e.g. + * for (...) { + * cond.andPrepare("SALESPROJECT_CLASSIFICATION.TYPE", entry, "# <> ?") + * } + * @return {String} the replaced SQL-condition string (replace # by the fieldname) + * @ignore + */ +SqlCondition.prototype._prepare = function(field, value, cond, fieldType) { + if (value == undefined) { - var pointPos = pField.indexOf("."); + throw new Error(translate.withArguments("${SQL_LIB_UNDEFINED_VALUE} field: %0", [field])); + } + + if (cond == undefined) { + cond = "# = ?" + } - if (pointPos > 0 && pointPos < pField.length-1) - alias = pField; + var alias; + + if (typeof field === 'string') + { + var pointPos = field.indexOf("."); + + if (pointPos > 0 && pointPos < field.length-1) + { + alias = field; + } else - throw new Error(translate.text("${SQL_LIB_FIELD_WRONG_FORMAT}") + pField + translate.withArguments("${SQL_LIB_FIELD_WRONG_FORMAT} field: %0", [pField])); + { + throw new Error(translate.text("${SQL_LIB_FIELD_WRONG_FORMAT}") + field + translate.withArguments("${SQL_LIB_FIELD_WRONG_FORMAT} field: %0", [field])); + } } else { - if (pField.length == 2) - pField.push(pField[0]); - - if (pField.length == 3) + if (field.length == 3) { - alias = pField[2] + "." + pField[1]; - pField = pField[0] + "." + pField[1]; + alias = field[2] + "." + field[1]; + field = field[0] + "." + field[1]; } else + { throw new Error(translate.text("${SQL_LIB_FIELD_WRONG_FORMAT}") + field.toSource() + translate.withArguments("${SQL_LIB_FIELD_WRONG_FORMAT} field: %0", [field.toSource()])); + } } - return [alias, pField] -} - -SqlUtils.replaceConditionTemplate = function(pCondition, pPlaceholder, pReplacement) -{ - //SqlUtils.replaceConditionTemplate(pCondition, '#', SqlUtils.parseField(pFieldOrCond).join(".")) + var type; + + if (fieldType == undefined) + fieldType = SqlUtils.getSingleColumnType(field, undefined, this.alias); + //this function looks more complex (and slower) than it actually is /* the following regex looks like this after javascript-escaping of the backslash: (?<!\\)((?:\\\\)*)# the regexp searches for the unescaped character and these characters are replaced by the field name @@ -4860,101 +4913,75 @@ SqlUtils.replaceConditionTemplate = function(pCondition, pPlaceholder, pReplacem --------------------- */ //use replaceAll because it's faster and supports negative lookbehinds - var replacements = {}; - //manually readd the replaced backslashes by using a group reference, because they a part of the match and therefore replaced by "replaceAll" - //since the field COULD contain already a group reference (I think this is extremely uncommon; - //probably that never happens but better stay save): escape that references within the fieldname - replacements["(?<!\\\\)((?:\\\\\\\\)*)" + pPlaceholder] = "$1" + text.replaceAll(pReplacement, { - "$1": "\\$1" - }) - //now that we've replaced the correct field placeholder let's replace the escaped placeholder sign "\#" to a normal placeholder sign "#" - replacements["\\\\" + pPlaceholder] = pPlaceholder + cond = text.replaceAll(cond, { + //manually readd the replaced backslashes by using a group reference, because they a part of the match and therefore replaced by "replaceAll" + //since the field COULD contain already a group reference (I think this is extremely uncommon; + //probably that never happens but better stay save): escape that references within the fieldname + "(?<!\\\\)((?:\\\\\\\\)*)#": "$1" + text.replaceAll(alias, { + "$1": "\\$1" + }), + //now that we've replaced the correct field placeholder let's replace the escaped number sign "\#" to a normal number sign "#" + "\\\\#": "#" + }); - return text.replaceAll(pCondition, replacements); + + + type = fieldType + this.preparedValues.push([value.toString(), type]); + return cond; } + /** - * Checks if the '#' is 0 or 1 time in pCondition, '?' has to be 1 time in pCondition. - * Also checks if '#' is before '?' - * @param {String} pCondition - * - * @return {Boolean} true if the format is ok + * function that resets the current SqlCondition as if no conditions would have been added + * this is usefull if you want to reuse the same object over and over + * @return {null} */ -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 (it uses text.replaceAll which supports lookbehind because it uses java) - 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@}")) +SqlCondition.prototype.clear = function() { + this._sqlStorage = ""; + this.preparedValues = []; + return this; } /** - * Escapes a jdito variable for the value of a SqlBuilder condition. SqlBuilder.prototype.where/and/or/... automatically resolve the value as a jdito - * variable if it starts with a single '$', so you can use this function to make sure the value is used as it is. - * <br> - * Note: The main purpose of this is to prevent errors resulting from unexpected user inputs. But if you are loading the input from a jdito variable - * anyways, you can just wite the variable name as the condition value and it will be safe. - * - * @param {String} pValue the value - * @return {String} the escaped string - * @example + * hidden function for initializing all properties for the sql conditions + * @return {null} * - * var sqlCond = newWhere("TABLE.COLUMN", SqlUtils.escapeVars(userInput)); //userInput could start with '$' + * @ignore */ -SqlUtils.escapeVars = function (pValue) -{ - if (typeof(pValue) == "string" && pValue.charAt(0) == "$") - return "$" + pValue; - return pValue; +SqlCondition.prototype._init = function() { + //init only wraps the clear function to avoid confusion in the constructor (and provide better extensibility) + return this.clear(); } -SqlUtils.getSqlConditionalOperator = function(pOperator) -{ - switch(parseInt(pOperator)) - { - case 1: - return SqlBuilder.EQUAL(); - case 2: - return SqlBuilder.NOT_EQUAL(); - - case 4: - return SqlBuilder.LESS(); - case 5: - return SqlBuilder.LESS_OR_EQUAL(); - - case 3: - return SqlBuilder.GREATER(); - case 6: - return SqlBuilder.GREATER_OR_EQUAL(); - - case 11: - return "# is not null"; - case 12: - return "# is null"; - - default: - throw new Error("Unsupported operator " + pOperator); - } -} +// some static functions for often used tasks. They are only provided for very simple tasks. /** - * Returns the pNullableExpr if pNullableExpr is not null - * otherwise it returns pDefaultExpr + * pField = pValue + * @param {String} pField the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" + * @param {String} pValue the value that shall be set into the prepared statement + * @param {String} [pAlternativeCond=""] Condition that is returned when nothing has been appended. + * @param {String} [pAlias=the current alias] the database alias where the condition shall be executed later (important for column types of preparedStatements) * - * @param {string|SqlBuilder} pNullableExpr a nullable expression - * @param {string|SqlBuilder} pDefaultExpr the default expression if pNullableExpr is null + * @return {Array[][][]} Prepared condition with [condition, [[field, type]]] * - * @returns {SqlBuilder._CaseStatement} pNullableExpr with pDefaultExpr as fallback + * @deprecated */ -SqlUtils.nullableWithDefault = function(pNullableExpr, pDefaultExpr) -{ - return SqlBuilder.caseStatement() - .when("(" + pNullableExpr.toString() + ") is null").then(pDefaultExpr) - .elseValue(pNullableExpr); +SqlCondition.equals = function(pField, pValue, pAlternativeCond, pAlias) { + return SqlCondition["begin"](pAlias).andPrepare(pField, pValue).build(pAlternativeCond); } +/** + * pField <> pValue + * @param {String} pField the database field as "tablename.columnname"; e.g. "ORGANISATION.NAME" + * @param {String} pValue the value that shall be set into the prepared statement + * @param {String} [pAlternativeCond=""] Condition that is returned when nothing has been appended. + * @param {String} [pAlias=the current alias] the database alias where the condition shall be executed later (important for column types of preparedStatements) + * + * @return {Array[][][]} Prepared condition with [condition, [[field, type]]] + * + * @deprecated + */ +SqlCondition.equalsNot = function(pField, pValue, pAlternativeCond, pAlias) { + return SqlCondition["begin"](pAlias).andPrepare(pField, pValue, "# <> ?").build(pAlternativeCond); +} \ No newline at end of file diff --git a/process/Util_lib/process.js b/process/Util_lib/process.js index 3ebc86d452..42c2763d2e 100644 --- a/process/Util_lib/process.js +++ b/process/Util_lib/process.js @@ -1138,6 +1138,33 @@ ArrayUtils.concatNonEmptyElements = function(pElement1, pElementN) return res; }; +/** + * Splits the elements of an array into chunks with a defined length + * + * @param {Array} pArray array to split + * @param {Number} pChunkSize desired length of the sub-arrays + * @return {Array} array of chunked arrays + * @example + * + * var arr = [1,2,3,4,5,6,7,8,9,10,11]; + * var chunks = ArrayUtils.chunk(arr, 4); + * logging.log(JSON.stringify(chunks)); //result: [[1,2,3,4],[5,6,7,8],[9,10,11]] + */ +ArrayUtils.chunk = function (pArray, pChunkSize) +{ + var chunkSize = Number(pChunkSize); + if (chunkSize <= 0) + { + return []; + } + var chunked = []; + for (let i = 0; i < pArray.length;) + { + chunked.push(pArray.slice(i, i += chunkSize)); + } + return chunked; +} + /** * Class containing utility functions for use with JSON * @class -- GitLab