docs: @Override
      protected String replaceGroupNameVariables(String str, List<String> groupNames,
              List<String> groupValues, boolean debugPrint) {
          if (debugPrint) {
              System.out.println("    In PostgreSQLBackend.replaceGroupNameVariables:");
              System.out.println("      str        : " + str);
              System.out.println("      groupNames : " + groupNames);
              System.out.println("      groupValues: " + groupValues);
          }
          // If any of the inputs are null or empty, then never mind - just
          // return the original String (str)
          if (str == null || groupNames == null || groupValues == null ||
                  str.isEmpty() || groupNames.isEmpty() || groupValues.isEmpty()) {
              return str;
          }
  
          // If a table was specified & found, use that; otherwise, just do a
          // simple text replacement, e.g., replace "{foo}" with "bar", where
          // "foo" is in the list of groupNames and "bar" is the corresponding
          // element in the list of groupValues.
          // (Note: this table represents the main table in an UPSERT statement,
          // e.g., "T1", in: UPSERT INTO T1 ...)
          String table = null;
          int index = groupNames.indexOf("table");
          if (index > -1 && index < groupValues.size()) {
              table = groupValues.get(index);
          } else {
              return replaceSimpleGroupNameVariables(str, groupNames, groupValues, debugPrint);
          }
  
          // If column values were specified & found, use those; otherwise, never
          // mind (Note: these column values may represent actual column values
          // specified in an UPSERT INTO VALUES statement; or column names or
          // expressions specified in an UPSERT INTO SELECT statement)
          List<String> columnValues = null;
          index = groupNames.indexOf("values");
          if (index > -1 && index < groupValues.size()) {
              String columnValuesString = groupValues.get(index);
              columnValues = new ArrayList<String>(Arrays.asList(columnValuesString.split("\\s*,\\s*")));
  
              // Handle the case where one or more of the commas were enclosed in
              // single quotes, so they should not have been used as separators
              // between the column values, meaning that some of the initially
              // defined column values need to be recombined
              if (columnValuesString.contains("'")) {
                  List<Integer> indexesToDelete = new ArrayList<Integer>();
                  for (int i=0; i < columnValues.size(); i++) {
                      if (hasOddNumberOfSingleQuotes(columnValues.get(i))) {
                          int j;
                          for (j=i+1; j < columnValues.size(); j++) {
                              columnValues.set(i, columnValues.get(i) + "," + columnValues.get(j));
                              indexesToDelete.add(j);
                              if (hasOddNumberOfSingleQuotes(columnValues.get(j))) {
                                  break;
                              }
                          }
                          i = j;
                      }
                  }
                  for (int k=indexesToDelete.size() - 1; k >= 0; k--) {
                      columnValues.remove(indexesToDelete.get(k).intValue());
                  }
              }
          } else {
              return str;
          }
  
          // Get the primary key column names, and the non-primary-key column
          // names, and all column names, for the specified table
          List<String> primaryKeyColumns = getPrimaryKeys(table);
          List<String> nonPrimaryKeyColumns = getNonPrimaryKeyColumns(table);
  
          // If one or more "select tables" was specified & found (that is, tables
          // used in the SELECT part of an UPSERT INTO T1 SELECT... statement),
          // use those; otherwise, this probably won't be needed, but use the main
          // (UPSERT INTO) table we already found, just in case
          List<String> selectTables = null;
          index = groupNames.indexOf("selecttables");
          if (index > -1 && index < groupValues.size()) {
              String selectTablesStr = groupValues.get(index).toUpperCase();
              if (selectTablesStr.contains(" JOIN ")) {
                  selectTables = new ArrayList<String>(Arrays.asList(selectTablesStr.split(" JOIN ")));
              } else {
                  selectTables = new ArrayList<String>(Arrays.asList(groupValues.get(index).split(",")));
              }
          } else {
              selectTables = Arrays.asList(table);
          }
  
          // If particular columns were specified, use only those; otherwise use
          // all columns (primary keys and non-primary-keys), as found above
          List<String> columns = null;
          index = groupNames.indexOf("columns");
          if (index > -1 && index < groupValues.size() && groupValues.get(index) != null) {
              columns = Arrays.asList(groupValues.get(index).split(","));
              // Lower-case table and column names are required for PostgreSQL;
              // we might need to alter this if we use another comparison
              // database (besides HSQL) someday
              for (int i=0; i < columns.size(); i++) {
                  columns.set(i, columns.get(i).trim().toLowerCase());
              }
              // Retain only those primary key columns that are in the specified
              // list of columns - in the specified column list order
              List<String> temp = new ArrayList<String>(columns);
              temp.retainAll(primaryKeyColumns);
              primaryKeyColumns = temp;
              // Retain only those non-primary-key columns that are in the specified
              // list of columns - in the specified column list order
              temp = new ArrayList<String>(columns);
              temp.retainAll(nonPrimaryKeyColumns);
              nonPrimaryKeyColumns = temp;
          } else {
              columns = getAllColumns(table);
          }
  
          // Handle the special case of UPSERT INTO ... SELECT *, i.e., when one
          // (or more??) of the "columnValues" is equal to "*"; go in reverse
          // order, to avoid messing up the list
          for (int i = columnValues.size() - 1; i >= 0; i--) {
              if (columnValues.get(i).trim().equals("*")) {
                  columnValues.remove(i);
                  for (int t = selectTables.size() - 1; t >= 0; t--) {
                      columnValues.addAll(i, getAllColumns(selectTables.get(t).trim()));
                  }
              }
          }
  
          // Remove elements from the "columnValues" list that correspond to
          // primary key columns, since those values do not need to be set in an
          // INSERT statement's ON CONFLICT ... DO UPDATE clause; but add them to
          // a separate list (which is needed only for an UPSERT INTO SELECT
          // statement); go in reverse order, to avoid messing up the list; the
          // "columnValues" and "columns" lists should have the same size here,
          // but just in case use the minimum
          List<String> pkColumnValues = new ArrayList<String>();
          for (int i = Math.min(columnValues.size(), columns.size()) - 1; i >= 0; i--) {
              if (primaryKeyColumns.contains(columns.get(i))) {
                  pkColumnValues.add(0, columnValues.get(i));
                  columnValues.remove(i);
              }
          }
  
          // If "where" was included in the "groupNames" (which it would be for an
          // UPSERT INTO SELECT statement), prepare a WHERE clause, setting primary
          // keys equal to their equivalent values in the main (_TMP) table, e.g.:
          //     "WHERE id=_TMP.id AND foo=_TMP.blah AND bar=_TMP.yada "
          String pkWhereClause = "EMPTY";
          if (groupNames.indexOf("where") > -1 && pkColumnValues.size() > 0
                  && primaryKeyColumns.size() > 0) {
              pkWhereClause = "WHERE " + pkColumnValues.get(0)+"=_TMP."+primaryKeyColumns.get(0)+" ";
              for (int i=1; i < pkColumnValues.size(); i++) {
                  pkWhereClause += "AND " + pkColumnValues.get(i)+"=_TMP."+primaryKeyColumns.get(i)+" ";
              }
          }
  
          // Replace the groupName "variables" with their corresponding
          // groupValues, processing special cases involving columnType
          // "pk" or "npk", as needed
          StringBuffer modified_str = new StringBuffer();
          Matcher matcher = groupNameVariables.matcher(str);
          while (matcher.find()) {
              String groupName = matcher.group("groupName");
              String columnType = matcher.group("columnType");
              // Filter this "variable" to only include primary key columns
              if ("pk".equalsIgnoreCase(columnType)) {
                  if ("columns".equalsIgnoreCase(groupName)) {
                      matcher.appendReplacement(modified_str, String.join(", ", primaryKeyColumns));
                  } else if ("where".equalsIgnoreCase(groupName)) {
                      matcher.appendReplacement(modified_str, pkWhereClause);
                  } else {
                      // No match: give up on this "variable"
                      matcher.appendReplacement(modified_str, "{"+groupName+":pk}");
                  }
              // Filter this "variable" to only include non-primary-key columns
              } else if ("npk".equalsIgnoreCase(columnType)) {
                  if ("columns".equalsIgnoreCase(groupName)) {
                      matcher.appendReplacement(modified_str, String.join(", ", nonPrimaryKeyColumns));
                  } else if ("values".equalsIgnoreCase(groupName)) {
                      // Extra escaping to make sure that "\\" remains as "\\" and
                      // "$" remains "$", despite appendReplacement's efforts to
                      // change them (not needed for column names, since they never
                      // contain those characters)
                      matcher.appendReplacement(modified_str,
                              String.join(", ", columnValues).replace("\\\\", "\\\\\\\\").replace("$", "\\$"));
                  } else {
                      // No match: give up on this "variable"
                      matcher.appendReplacement(modified_str, "{"+groupName+":npk}");
                  }
              // Simply return the value of this "variable"
              } else {
                  index = groupNames.indexOf(groupName);
                  if (index > -1 && index < groupValues.size()) {
                      String groupValue = groupValues.get(index);
                      matcher.appendReplacement(modified_str, (groupValue == null ? "" : groupValue));
                  } else {
                      // No match: give up on this "variable"
                      matcher.appendReplacement(modified_str, "{"+groupName+"}");
                  }
              }
          }
          matcher.appendTail(modified_str);
          if (debugPrint) {
              System.out.println("      table               : " + table);
              System.out.println("      columnValues        : " + columnValues);
              System.out.println("      primaryKeyColumns   : " + primaryKeyColumns);
              System.out.println("      nonPrimaryKeyColumns: " + nonPrimaryKeyColumns);
              System.out.println("      selectTables        : " + selectTables);
              System.out.println("      columns             : " + columns);
              System.out.println("      pkColumnValues      : " + pkColumnValues);
              System.out.println("      pkWhereClause       : " + pkWhereClause);
              System.out.println("      str                 : " + str);
              System.out.println("      modified_str        : " + modified_str);
          }
          return modified_str.toString();
      }