=== modified file 'src/com/goldencode/p2j/persist/Persistence.java'
--- src/com/goldencode/p2j/persist/Persistence.java	2022-06-08 09:37:27 +0000
+++ src/com/goldencode/p2j/persist/Persistence.java	2022-07-12 15:57:49 +0000
@@ -567,6 +567,7 @@
 **     IAS 20220324          Re-worked LockTableUpdater.
 **     OM  20220516          Added collation support for _meta databases (use the H2 dialect).
 **     IAS 20220608          Provide additional information in reportUDFVersion().
+**     IAS 20220712          Create missing UDFs
 */
 
 /*
@@ -632,6 +633,7 @@
 import java.util.logging.*;
 import com.goldencode.cache.*;
 import com.goldencode.p2j.main.*;
+import com.goldencode.p2j.persist.deploy.*;
 import com.goldencode.p2j.persist.dialect.*;
 import com.goldencode.p2j.persist.event.*;
 import com.goldencode.p2j.persist.hql.*;
@@ -1021,22 +1023,26 @@
       String searchPath = "unknown";
       
       try (Session session = new Session(database);
-           Statement st = session.getConnection().createStatement())
+           Connection conn = session.getConnection())
       {
          String sqlFunc = HQLPreprocessor.getRegisteredFunction(database, "getFWDVersion", null);
          if (sqlFunc == null)
          {
             sqlFunc = "getFWDVersion"; // the function is not overloaded for all dialects
          }
-         useSQLUdfs = (dialect instanceof P2JPostgreSQLDialect) &&
+         useSQLUdfs = dialect.isNativeUDFsSupported() &&
                   !DatabaseManager.getConfiguration(database).isUseJavaUDFs();
-         if (useSQLUdfs && !sqlFunc.startsWith("udf."))
+         if (useSQLUdfs)
          {
-            sqlFunc = "udf." + sqlFunc;
+            ScriptRunner.createMissingUdfs(database);
+            if (!sqlFunc.startsWith("udf."))
+            {
+               sqlFunc = "udf." + sqlFunc;
+            }
          }
-         try
+         try(Statement st = conn.createStatement(); 
+            ResultSet rs = st.executeQuery("SELECT " + sqlFunc + "()"))
          {
-            ResultSet rs = st.executeQuery("SELECT " + sqlFunc + "()");
             if (rs.next())
             {
                String val = rs.getString(1);
@@ -1054,10 +1060,10 @@
          }
          if (dialect instanceof P2JPostgreSQLDialect)
          {
-            try
+            try(Statement st = conn.createStatement(); 
+                ResultSet rs = st.executeQuery(
+                              "SELECT setting FROM pg_settings WHERE name='search_path'"))
             {
-               ResultSet rs = st.executeQuery(
-                        "SELECT setting FROM pg_settings WHERE name='search_path'");
                if (rs.next())
                {
                   String val = rs.getString(1);

=== modified file 'src/com/goldencode/p2j/persist/deploy/ScriptRunner.java'
--- src/com/goldencode/p2j/persist/deploy/ScriptRunner.java	2022-06-14 23:36:04 +0000
+++ src/com/goldencode/p2j/persist/deploy/ScriptRunner.java	2022-07-12 15:58:16 +0000
@@ -10,6 +10,7 @@
 **     IAS 20220608 Add option for setting database search_path.
 **     IAS 20220610 Create 'scale' UDF if PostgreSQL major version is < 10.
 **     OM  20220614 Dropped unused import. Formatting javadoc and typo fixes.
+**     IAS 20220712 Create missing UDFs
 */
 
 /*
@@ -74,8 +75,10 @@
 import java.util.regex.*;
 import java.util.stream.*;
 import com.goldencode.p2j.cfg.*;
+import com.goldencode.p2j.persist.*;
 import com.goldencode.p2j.persist.deploy.ScriptSplitter.*;
 import com.goldencode.p2j.persist.dialect.*;
+import com.goldencode.p2j.persist.orm.*;
 import com.goldencode.p2j.util.*;
 
 /** Run SQL script against PostgreSQL database */
@@ -112,7 +115,21 @@
          "$function$"
       )
    );
-   
+
+   private static final List<String> COUNT = Collections.unmodifiableList(
+            Arrays.asList(
+               "select count(*)",
+               "from pg_proc p",
+               "left join pg_namespace n on p.pronamespace = n.oid",
+               "left join pg_language l on p.prolang = l.oid",
+               "left join pg_type t on t.oid = p.prorettype",
+               "where n.nspname = ?",
+               "  and l.lanname in ('sql', 'plpgsql')",
+               "  and t.typname != 'trigger'",
+               "  and p.proname = ?"
+            )
+         );
+
    /**
     * Runner main method.
     *
@@ -136,8 +153,7 @@
       {
          throw new IllegalArgumentException("Unknown JDBC database type: [" + dbtype + "]");
       }
-      ScriptSplitter splitter = dialect.scripSplitter();
-      List<String> scriptNames;
+      List<String> scripts;
       boolean setSearchPath = false; 
       boolean createScale = false;
       switch (args[3])
@@ -145,17 +161,14 @@
          case "udf.install.search_path":
             setSearchPath = true;
          case "udf.install":
-            String[] scripts;
             if (args.length > 4)
             {
-               scripts = args[4].split("\\s+");
+               scripts = Arrays.asList(args[4].split("\\s+"));
             }
             else
             {
-               scripts = new String[] {"udfs.sql", "words-udfs-sql.sql"}; 
+               scripts = Arrays.asList("udfs.sql", "words-udfs-sql.sql"); 
             }
-            scriptNames = Arrays.stream(scripts).map(s -> "/udf/" + dbtype + "/" + s).
-                     collect(Collectors.toList());
             break;
          default:
             throw new IllegalArgumentException("Unknown command: [" + args[3] + "]");
@@ -164,46 +177,122 @@
       
       try(Connection conn = DriverManager.getConnection(args[0], args[1], args[2]))
       {
-         boolean isPostgreSQL =  "postgresql".equalsIgnoreCase(
-                  conn.getMetaData().getDatabaseProductName());
-         if (isPostgreSQL && getPgVersion(conn) < 1000)
-         {
-            createScaleUDF(conn);
-         }
-         
-         for (String scriptName: scriptNames)
-         {
-            InputStream is = ScriptRunner.class.getResourceAsStream(scriptName);
-            if (is == null)
-            {
-               throw new IllegalStateException("Cannot open script [" + scriptName + "]" );
-            }
-            try(BufferedReader br = new BufferedReader(new InputStreamReader(is)))
-            {
-               LOG.log(Level.INFO, "Running script: [{}]", scriptName);
-               runScript(conn, br, splitter);
-            }
-         }
-         if (setSearchPath && isPostgreSQL)
-         {
-            String dbname = null;
-            try(Statement stmt = conn.createStatement())
-            {
-               ResultSet rs = stmt.executeQuery("SELECT current_database()");
-               if( rs.next())
-               {
-                  dbname = rs.getString(1);
-               }
-            }
-            if (dbname != null)
-            {
-               LOG.log(Level.INFO, "Setting search_path for [{}]", dbname);
-               execute(conn, "ALTER DATABASE " + dbname + " SET search_path TO public,udf");
-            }
-            else
-            {
-               LOG.log(Level.WARNING, "Failed to retrieve the database name");
-            }
+         apply(dialect, scripts, setSearchPath, conn);
+      }
+   }
+   
+   /**
+    * Check for missing UDFs and create them.
+    * 
+    * @param db
+    *       Database.
+    * @throws SQLException
+    *         on SQL error.
+    * @throws PersistenceException
+    *         on script reading error.
+    */
+   public static void createMissingUdfs(Database db) 
+   throws SQLException, 
+          PersistenceException
+   {
+      String dbname = db.getName();
+      Dialect dialect = DatabaseManager.getDialect(db);
+      Settings orm = DatabaseManager.getConfiguration(db).getOrmSettings();
+      String url = orm.getString(Settings.URL, null);
+      String adm = orm.getString(Settings.ADMIN, null);
+      String pwd = orm.getString(Settings.ADMIN_PASSWORD, null);
+      if (adm == null || pwd == null)
+      {
+         LOG.info("No admin credentials provided, skipping missing UDFs analysis");
+         return;
+      }
+      List<String> scripts = new ArrayList<>();
+      try(Connection conn = DriverManager.getConnection(url, adm, pwd))
+      {
+         if (countUDFs(conn, "udf", "getfwdversion") == 0)
+         {
+            LOG.info(String.format("%s UDF not found in %s; %s SQL script will be applied", 
+                     "'udf.getfwdversion'", dbname, "'udfs.sql'"));
+            scripts.add("udfs.sql");
+         }
+         if (countUDFs(conn, "public", "words") < 2)
+         {
+            LOG.info(String.format("Less than 2 %s UDFs found in %s; %s SQL script will be applied", 
+                     "'public.words'", dbname, "'words-udfs-sql.sql'"));
+            scripts.add("words-udfs-sql.sql");
+         }
+         try
+         {
+            apply(dialect, scripts, false, conn);
+         }
+         catch (IOException e)
+         {
+            throw new PersistenceException(e);
+         }
+      }
+   }
+
+   /**
+    * Apply SQL scripts.
+    * @param dialect
+    *        Database dialect.
+    * @param scripts
+    *        Scripts to be applied.
+    * @param setSearchPath
+    *        Flag indicating that serach path should be set.  
+    * @param conn
+    *        Database connection.
+    * @throws SQLException
+    *         on SQL error.
+    * @throws IOException
+    *         on script reading error.
+    */
+   private static void apply(Dialect dialect, List<String> scripts,
+            boolean setSearchPath, Connection conn) throws SQLException, IOException
+   {
+      ScriptSplitter splitter = dialect.scripSplitter();
+      List<String> scriptNames = scripts.stream().
+               map(s -> "/udf/" + dialect.getJdbcType() + "/" + s).
+               collect(Collectors.toList());
+      boolean isPostgreSQL =  "postgresql".equalsIgnoreCase(
+               conn.getMetaData().getDatabaseProductName());
+      if (isPostgreSQL && getPgVersion(conn) < 1000)
+      {
+         createScaleUDF(conn);
+      }
+      
+      for (String scriptName: scriptNames)
+      {
+         InputStream is = ScriptRunner.class.getResourceAsStream(scriptName);
+         if (is == null)
+         {
+            throw new IllegalStateException("Cannot open script [" + scriptName + "]" );
+         }
+         try(BufferedReader br = new BufferedReader(new InputStreamReader(is)))
+         {
+            LOG.log(Level.INFO, String.format("Running script: [%s]", scriptName));
+            runScript(conn, br, splitter);
+         }
+      }
+      if (setSearchPath && isPostgreSQL)
+      {
+         String dbname = null;
+         try(Statement stmt = conn.createStatement();
+             ResultSet rs = stmt.executeQuery("SELECT current_database()"))
+         {
+            if( rs.next())
+            {
+               dbname = rs.getString(1);
+            }
+         }
+         if (dbname != null)
+         {
+            LOG.log(Level.INFO, String.format("Setting search_path for [%s]", dbname));
+            execute(conn, "ALTER DATABASE " + dbname + " SET search_path TO public,udf");
+         }
+         else
+         {
+            LOG.log(Level.WARNING, "Failed to retrieve the database name");
          }
       }
    }
@@ -351,7 +440,7 @@
     *          The database connection.
     *
     * @throws  SQLException
-    *          Oon error.
+    *          On error.
     */
    private static void createScaleUDF(Connection conn)
    throws SQLException
@@ -361,4 +450,36 @@
       String sql = SCALE.stream().collect(Collectors.joining(eoln));
       execute(conn, sql);
    }
+
+   /**
+    * Count number of UDFs with a given name.
+    * @param   conn
+    *          The database connection.
+    * @param   schema
+    *          schema name.
+    * @param   name
+    *          UDF name.
+    * @return  number of UDFs with a given name.         
+    * @throws  SQLException
+    *          On error.
+    */
+   private static int countUDFs(Connection conn, String schema, String name)
+   throws SQLException
+   {
+      String eoln = EnvironmentOps.OS_WIN.equalsIgnoreCase(Configuration.getParameter("opsys"))
+            ? "\r\n" /* express to WINDOWS */ : "\n" /* default to Linux */;  
+      String sql = COUNT.stream().collect(Collectors.joining(eoln));
+      try (PreparedStatement pstmt = conn.prepareStatement(sql))
+      {
+         int n = 0;
+         pstmt.setString(1, schema);
+         pstmt.setString(2, name);
+         ResultSet rs = pstmt.executeQuery();
+         if (rs.next())
+         {
+            n = rs.getInt(1);
+         }
+         return n;
+      }
+   }
 }

=== modified file 'src/com/goldencode/p2j/persist/dialect/Dialect.java'
--- src/com/goldencode/p2j/persist/dialect/Dialect.java	2022-06-02 09:08:05 +0000
+++ src/com/goldencode/p2j/persist/dialect/Dialect.java	2022-07-12 15:54:37 +0000
@@ -78,7 +78,8 @@
 **     OM  20210412          Replace Hibernate-specific casts with dialect-specific casts
 **     IAS 20211223          Special processing of errors raised by UDFs. 
 **     IAS 20220112          Resolve dialect by JDBC database type. 
-**     IAS 20220602          Added  isNativeUDFsSupported() method
+**     IAS 20220602          Added isNativeUDFsSupported() method.
+**     IAS 20220712          Added getJdbcType() method.
 */
 
 /*
@@ -315,6 +316,12 @@
     */
    public abstract ScriptSplitter scripSplitter();
    
+   /** 
+    * Get the dialect JDBC type.
+    * @return dialect JDBC type.
+    */
+   public abstract String getJdbcType();
+
    /**
     * Create a {@link Blob} instance from the specified bytes.
     * 

=== modified file 'src/com/goldencode/p2j/persist/dialect/P2JH2Dialect.java'
--- src/com/goldencode/p2j/persist/dialect/P2JH2Dialect.java	2022-06-02 09:08:05 +0000
+++ src/com/goldencode/p2j/persist/dialect/P2JH2Dialect.java	2022-07-12 15:55:05 +0000
@@ -94,7 +94,8 @@
 **     OM  20210908          Used SchemaDictionary.TEMP_TABLE_DB instead of hardcoded constant.
 **     IAS 20211223          Special processing of errors raised by UDFs. 
 **     IAS 20220112          Resolve dialect by JDBC database type. 
-**     IAS 20220602          Added  isNativeUDFsSupported() method
+**     IAS 20220602          Added isNativeUDFsSupported() method.
+**     IAS 20220712          Added getJdbcType() method.
 */
 
 /*
@@ -1409,6 +1410,15 @@
    }
    
 
+   /** 
+    * Get the dialect JDBC type.
+    * @return dialect JDBC type.
+    */
+   public String getJdbcType()
+   {
+      return "h2";
+   }
+   
    /**
     * Create a {@link Blob} instance from the specified bytes.
     * 

=== modified file 'src/com/goldencode/p2j/persist/dialect/P2JPostgreSQLDialect.java'
--- src/com/goldencode/p2j/persist/dialect/P2JPostgreSQLDialect.java	2022-06-02 09:08:05 +0000
+++ src/com/goldencode/p2j/persist/dialect/P2JPostgreSQLDialect.java	2022-07-12 15:55:23 +0000
@@ -114,7 +114,8 @@
 **     IAS 20220112          Resolve dialect by JDBC database type.
 **     TJD 20220331          Removed reference to IOUtils.toInputStream
 **     IAS 20220601          Do not emit 'words' UDFs definitions
-**     IAS 20220602          Added  isNativeUDFsSupported() method
+**     IAS 20220602          Added isNativeUDFsSupported() method.
+**     IAS 20220712          Added getJdbcType() method.
 */
 
 /*
@@ -1776,6 +1777,15 @@
       return new PgScriptSplitter();
    }
 
+   /** 
+    * Get the dialect JDBC type.
+    * @return dialect JDBC type.
+    */
+   public String getJdbcType()
+   {
+      return "postgresql";
+   }
+
    /**
     * Create a {@link Blob} instance from the specified bytes.
     * 

=== modified file 'src/com/goldencode/p2j/persist/dialect/P2JSQLServer2008Dialect.java'
--- src/com/goldencode/p2j/persist/dialect/P2JSQLServer2008Dialect.java	2022-06-02 09:08:05 +0000
+++ src/com/goldencode/p2j/persist/dialect/P2JSQLServer2008Dialect.java	2022-07-12 15:55:49 +0000
@@ -34,7 +34,8 @@
 **     CA  20210305 The object and handle fields have a Long representation at the SQL table field.
 **     IAS 20211223 Special processing of errors raised by UDFs (placeholder). 
 **     IAS 20220112 Resolve dialect by JDBC database type. 
-**     IAS 20220602          Added  isNativeUDFsSupported() method
+**     IAS 20220602 Added isNativeUDFsSupported() method.
+**     IAS 20220712 Added getJdbcType() method.
 */
 
 /*
@@ -1234,6 +1235,15 @@
       throw new UnsupportedOperationException();
    }
 
+   /** 
+    * Get the dialect JDBC type.
+    * @return dialect JDBC type.
+    */
+   public String getJdbcType()
+   {
+      return "sqlserver";
+   }
+
    /**
     * Create a {@link Blob} instance from the specified bytes.
     * 

=== modified file 'src/com/goldencode/p2j/persist/orm/Settings.java'
--- src/com/goldencode/p2j/persist/orm/Settings.java	2020-12-20 17:05:05 +0000
+++ src/com/goldencode/p2j/persist/orm/Settings.java	2022-07-12 15:53:13 +0000
@@ -2,7 +2,7 @@
 ** Module   : Settings.java
 ** Abstract : Configuration information required for the ORM framework
 **
-** Copyright (c) 2019-2020, Golden Code Development Corporation.
+** Copyright (c) 2019-2022, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------Description---------------------------------
 ** 001 ECF 20191001 Created initial version. ORM layer configuration information.
@@ -10,6 +10,7 @@
 ** 003 AIL 20200918 Made Settings use the dynamically computed URL for per-session databases.
 ** 004 AIL 20201016 Increased searching interval for getAllForCategory.
 ** 005 IAS 20201219 Added 'CPSTREAM' import task argument
+** 006 IAS 20220712 Added admin login/pwd values.
 */
 
 /*
@@ -95,6 +96,12 @@
    /** Connection password key */
    public static final String PASSWORD = "connection.password";
    
+   /** Connection uadmin name key */
+   public static final String ADMIN = "connection.admin_username";
+   
+   /** Connection password key */
+   public static final String ADMIN_PASSWORD = "connection.admin_password";
+
    /** table dump file codepage, optional */
    public static final String CPSTREAM = "dump.cpstream";
 

