You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@madlib.apache.org by kh...@apache.org on 2020/05/21 16:47:09 UTC

[madlib] 01/02: DL: Add utility function for custom functions

This is an automated email from the ASF dual-hosted git repository.

khannaekta pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/madlib.git

commit 5071d8b94d3fd7a24853bc41ad920dbfe168846e
Author: Ekta Khanna <ek...@pivotal.io>
AuthorDate: Wed May 13 15:51:55 2020 -0700

    DL: Add utility function for custom functions
    
    This commit adds new helper functions for loading and deleting from a
    given table custom functions that can be passed to keras fit/evaluate.
    The user passes in the function name along with a valid dill pickled
    object. This commit also adds dev-check tests for it.
---
 .../madlib_keras_custom_function.py_in             | 245 +++++++++++++++++++++
 .../madlib_keras_custom_function.sql_in            | 103 +++++++++
 .../test/madlib_keras_custom_function.sql_in       | 148 +++++++++++++
 tool/docker/base/Dockerfile_postgres_10_Jenkins    |   2 +-
 4 files changed, 497 insertions(+), 1 deletion(-)

diff --git a/src/ports/postgres/modules/deep_learning/madlib_keras_custom_function.py_in b/src/ports/postgres/modules/deep_learning/madlib_keras_custom_function.py_in
new file mode 100644
index 0000000..246c72d
--- /dev/null
+++ b/src/ports/postgres/modules/deep_learning/madlib_keras_custom_function.py_in
@@ -0,0 +1,245 @@
+# coding=utf-8
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+
+import dill
+import plpy
+from plpy import spiexceptions
+from utilities.control import MinWarning
+from utilities.utilities import _assert
+from utilities.utilities import get_col_name_type_sql_string
+from utilities.validate_args import columns_missing_from_table
+from utilities.validate_args import input_tbl_valid
+from utilities.validate_args import quote_ident
+from utilities.validate_args import table_exists
+
+module_name = 'Keras Custom Function'
+class CustomFunctionSchema:
+    """Expected format of custom function table.
+       Example uses:
+
+           from utilities.validate_args import columns_missing_from_table
+           from madlib_keras_custom_function import CustomFunctionSchema
+
+           # Validate names in cols list against actual table
+           missing_cols = columns_missing_from_table('my_custom_fn_table', CustomFunctionSchema.col_names)
+
+           # Get function object from table, without hard coding column names
+           sql = "SELECT {object} FROM {table} WHERE {id} = {my_id}"
+                 .format(object=CustomFunctionSchema.FN_OBJ,
+                         table='my_custom_fn_table',
+                         id=CustomFunctionSchema.FN_ID,
+                         my_id=1)
+           object = plpy.execute(sql)[0]
+
+    """
+    FN_ID = 'id'
+    FN_NAME = 'name'
+    FN_OBJ = 'object'
+    FN_DESC = 'description'
+    col_names = (FN_ID, FN_NAME, FN_DESC, FN_OBJ)
+    col_types = ('SERIAL', 'TEXT', 'TEXT', 'BYTEA')
+
+def _validate_object(object, **kwargs):
+    _assert(object is not None, "{0}: function object cannot be NULL!".format(module_name))
+    try:
+        obj=dill.loads(object)
+    except Exception as e:
+        plpy.error("{0}: Invalid function object".format(module_name, e))
+
+@MinWarning("error")
+def load_custom_function(object_table, object, name, description=None, **kwargs):
+    object_table = quote_ident(object_table)
+    _validate_object(object)
+    _assert(name is not None,
+            "{0}: function name cannot be NULL!".format(module_name))
+    if not table_exists(object_table):
+        col_defs = get_col_name_type_sql_string(CustomFunctionSchema.col_names,
+                                                CustomFunctionSchema.col_types)
+
+        sql = "CREATE TABLE {0} ({1}, PRIMARY KEY({2}))" \
+            .format(object_table, col_defs, CustomFunctionSchema.FN_NAME)
+
+        plpy.execute(sql, 0)
+        plpy.info("{0}: Created new custom function table {1}." \
+                  .format(module_name, object_table))
+    else:
+        missing_cols = columns_missing_from_table(object_table,
+                                                  CustomFunctionSchema.col_names)
+        if len(missing_cols) > 0:
+            plpy.error("{0}: Invalid custom function table {1},"
+                       " missing columns: {2}".format(module_name,
+                                                      object_table,
+                                                      missing_cols))
+
+    insert_query = plpy.prepare("INSERT INTO {0} "
+                                "VALUES(DEFAULT, $1, $2, $3);".format(object_table),
+                                CustomFunctionSchema.col_types[1:])
+    try:
+        plpy.execute(insert_query,[name, description, object], 0)
+    # spiexceptions.UniqueViolation is only supported for PG>=9.2. For
+    # GP5(based of PG8.4) it cannot be used. Therefore, checking exception
+    # message for duplicate key error.
+    except Exception as e:
+        if 'duplicate key' in e.message:
+            plpy.error("Function '{0}' already exists in {1}".format(name, object_table))
+        plpy.error(e)
+
+    plpy.info("{0}: Added function {1} to {2} table".
+              format(module_name, name, object_table))
+
+@MinWarning("error")
+def delete_custom_function(object_table, id=None, name=None, **kwargs):
+    object_table = quote_ident(object_table)
+    input_tbl_valid(object_table, "Keras Custom Funtion")
+    _assert(id is not None or name is not None,
+            "{0}: function id/name cannot be NULL! " \
+            "Use \"SELECT delete_custom_function('usage')\" for help.".format(module_name))
+
+    missing_cols = columns_missing_from_table(object_table, CustomFunctionSchema.col_names)
+    if len(missing_cols) > 0:
+        plpy.error("{0}: Invalid custom function table {1},"
+                   " missing columns: {2}".format(module_name, object_table,
+                                                  missing_cols))
+
+    if id is not None:
+        sql = """
+               DELETE FROM {0} WHERE {1}={2}
+              """.format(object_table, CustomFunctionSchema.FN_ID, id)
+    else:
+        sql = """
+               DELETE FROM {0} WHERE {1}=$${2}$$
+              """.format(object_table, CustomFunctionSchema.FN_NAME, name)
+    res = plpy.execute(sql, 0)
+
+    if res.nrows() > 0:
+        plpy.info("{0}: Object id {1} has been deleted from {2}.".
+                  format(module_name, id, object_table))
+    else:
+        plpy.error("{0}: Object id {1} not found".format(module_name, id))
+
+    sql = "SELECT {0} FROM {1}".format(CustomFunctionSchema.FN_ID, object_table)
+    res = plpy.execute(sql, 0)
+    if not res:
+        plpy.info("{0}: Dropping empty custom keras function table " \
+                  "table {1}".format(module_name, object_table))
+        sql = "DROP TABLE {0}".format(object_table)
+        plpy.execute(sql, 0)
+
+class KerasCustomFunctionDocumentation:
+    @staticmethod
+    def _returnHelpMsg(schema_madlib, message, summary, usage, method):
+        if not message:
+            return summary
+        elif message.lower() in ('usage', 'help', '?'):
+            return usage
+        return """
+            No such option. Use "SELECT {schema_madlib}.{method}()"
+            for help.
+        """.format(**locals())
+
+    @staticmethod
+    def load_custom_function_help(schema_madlib, message):
+        method = "load_custom_function"
+        summary = """
+        ----------------------------------------------------------------
+                            SUMMARY
+        ----------------------------------------------------------------
+        The user can specify custom functions as part of the parameters
+        passed to madlib_keras_fit()/madlib_keras_fit_multiple(). These
+        custom function(s) definition must be stored in a table to pass.
+        This is a helper function to help users insert object(BYTEA) of
+        the function definitions into a table.
+        If the output table already exists, the custom function specified
+        will be added as a new row into the table. The output table could
+        thus act as a repository of Keras custom functions.
+
+        For more details on function usage:
+        SELECT {schema_madlib}.{method}('usage')
+        """.format(**locals())
+
+        usage = """
+        ---------------------------------------------------------------------------
+                                        USAGE
+        ---------------------------------------------------------------------------
+        SELECT {schema_madlib}.{method}(
+            object_table,       --  VARCHAR. Output table to load custom function.
+            object,             --  BYTEA. dill pickled object of the function definition.
+            name,               --  TEXT. Free text string to identify a name
+            description         --  TEXT. Free text string to provide a description
+        );
+
+
+        ---------------------------------------------------------------------------
+                                        OUTPUT
+        ---------------------------------------------------------------------------
+        The output table produced by load_custom_function contains the following columns:
+
+        'id'                    -- SERIAL. Function ID.
+        'name'                  -- TEXT PRIMARY KEY. unique function name.
+        'description'           -- TEXT. function description.
+        'object'                -- BYTEA. dill pickled function object.
+
+        """.format(**locals())
+
+        return KerasCustomFunctionDocumentation._returnHelpMsg(
+            schema_madlib, message, summary, usage, method)
+    # ---------------------------------------------------------------------
+
+    @staticmethod
+    def delete_custom_function_help(schema_madlib, message):
+        method = "delete_custom_function"
+        summary = """
+        ----------------------------------------------------------------
+                            SUMMARY
+        ----------------------------------------------------------------
+        Delete the custom function corresponding to the provided id
+        from the custom function repository table (object_table).
+
+        For more details on function usage:
+        SELECT {schema_madlib}.{method}('usage')
+        """.format(**locals())
+
+        usage = """
+        ---------------------------------------------------------------------------
+                                        USAGE
+        ---------------------------------------------------------------------------
+        SELECT {schema_madlib}.{method}(
+            object_table     VARCHAR, -- Table containing keras custom function objects.
+            id               INTEGER  -- The id of the keras custom function object
+                                         to be deleted.
+        );
+
+        SELECT {schema_madlib}.{method}(
+            object_table     VARCHAR, -- Table containing keras custom function objects.
+            name             TEXT     -- Function name of the keras custom function
+                                         object to be deleted.
+        );
+
+        ---------------------------------------------------------------------------
+                                        OUTPUT
+        ---------------------------------------------------------------------------
+        This method deletes the row corresponding to the given id in the
+        object_table. This also tries to drop the table if the table is
+        empty after dropping the id. If there are any views depending on the
+        table, a warning message is displayed and the table is not dropped.
+
+        ---------------------------------------------------------------------------
+        """.format(**locals())
+
+        return KerasCustomFunctionDocumentation._returnHelpMsg(
+            schema_madlib, message, summary, usage, method)
diff --git a/src/ports/postgres/modules/deep_learning/madlib_keras_custom_function.sql_in b/src/ports/postgres/modules/deep_learning/madlib_keras_custom_function.sql_in
new file mode 100644
index 0000000..62f89a9
--- /dev/null
+++ b/src/ports/postgres/modules/deep_learning/madlib_keras_custom_function.sql_in
@@ -0,0 +1,103 @@
+/* ----------------------------------------------------------------------- *//**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ *
+ * @file madlib_keras_custom_function.sql_in
+ *
+ * @brief SQL functions for load/delete keras custom function objects
+ * @date August 2019
+ *
+ *
+ *//* ----------------------------------------------------------------------- */
+
+m4_include(`SQLCommon.m4')
+
+CREATE OR REPLACE FUNCTION MADLIB_SCHEMA.load_custom_function(
+    object_table            VARCHAR,
+    object                  BYTEA,
+    name                    TEXT,
+    description             TEXT
+) RETURNS VOID AS $$
+    PythonFunctionBodyOnly(`deep_learning', `madlib_keras_custom_function')
+    with AOControl(False):
+        madlib_keras_custom_function.load_custom_function(**globals())
+$$ LANGUAGE plpythonu VOLATILE
+m4_ifdef(`__HAS_FUNCTION_PROPERTIES__', `MODIFIES SQL DATA', `');
+
+CREATE OR REPLACE FUNCTION MADLIB_SCHEMA.load_custom_function(
+    object_table            VARCHAR,
+    object                  BYTEA,
+    name                    TEXT
+) RETURNS VOID AS $$
+    SELECT MADLIB_SCHEMA.load_custom_function($1, $2, $3, NULL)
+$$ LANGUAGE sql VOLATILE
+m4_ifdef(`__HAS_FUNCTION_PROPERTIES__', `MODIFIES SQL DATA', `');
+
+-- Functions for online help
+CREATE OR REPLACE FUNCTION MADLIB_SCHEMA.load_custom_function(
+    message VARCHAR
+) RETURNS VARCHAR AS $$
+    PythonFunctionBodyOnly(deep_learning, madlib_keras_custom_function)
+    return madlib_keras_custom_function.KerasCustomFunctionDocumentation.load_custom_function_help(schema_madlib, message)
+$$ LANGUAGE plpythonu VOLATILE
+m4_ifdef(`__HAS_FUNCTION_PROPERTIES__', `MODIFIES SQL DATA', `');
+
+CREATE OR REPLACE FUNCTION MADLIB_SCHEMA.load_custom_function()
+RETURNS VARCHAR AS $$
+    PythonFunctionBodyOnly(deep_learning, madlib_keras_custom_function)
+    return madlib_keras_custom_function.KerasCustomFunctionDocumentation.load_custom_function_help(schema_madlib, '')
+$$ LANGUAGE plpythonu VOLATILE
+m4_ifdef(`__HAS_FUNCTION_PROPERTIES__', `MODIFIES SQL DATA', `');
+
+-- Function to delete a keras custom function from object table
+CREATE OR REPLACE FUNCTION MADLIB_SCHEMA.delete_custom_function(
+    object_table VARCHAR,
+    id INTEGER
+)
+RETURNS VOID AS $$
+    PythonFunctionBodyOnly(`deep_learning',`madlib_keras_custom_function')
+    with AOControl(False):
+        madlib_keras_custom_function.delete_custom_function(object_table, id=id)
+$$ LANGUAGE plpythonu VOLATILE;
+
+CREATE OR REPLACE FUNCTION MADLIB_SCHEMA.delete_custom_function(
+    object_table VARCHAR,
+    name TEXT
+)
+RETURNS VOID AS $$
+    PythonFunctionBodyOnly(`deep_learning',`madlib_keras_custom_function')
+    with AOControl(False):
+        madlib_keras_custom_function.delete_custom_function(object_table, name=name)
+$$ LANGUAGE plpythonu VOLATILE;
+
+-- Functions for online help
+CREATE OR REPLACE FUNCTION MADLIB_SCHEMA.delete_custom_function(
+    message VARCHAR
+) RETURNS VARCHAR AS $$
+    PythonFunctionBodyOnly(deep_learning, madlib_keras_custom_function)
+    return madlib_keras_custom_function.KerasCustomFunctionDocumentation.delete_custom_function_help(schema_madlib, message)
+$$ LANGUAGE plpythonu VOLATILE
+m4_ifdef(`__HAS_FUNCTION_PROPERTIES__', `MODIFIES SQL DATA', `');
+
+CREATE OR REPLACE FUNCTION MADLIB_SCHEMA.delete_custom_function()
+RETURNS VARCHAR AS $$
+    PythonFunctionBodyOnly(deep_learning, madlib_keras_custom_function)
+    return madlib_keras_custom_function.KerasCustomFunctionDocumentation.delete_custom_function_help(schema_madlib, '')
+$$ LANGUAGE plpythonu VOLATILE
+m4_ifdef(`__HAS_FUNCTION_PROPERTIES__', `MODIFIES SQL DATA', `');
diff --git a/src/ports/postgres/modules/deep_learning/test/madlib_keras_custom_function.sql_in b/src/ports/postgres/modules/deep_learning/test/madlib_keras_custom_function.sql_in
new file mode 100644
index 0000000..74f6ba2
--- /dev/null
+++ b/src/ports/postgres/modules/deep_learning/test/madlib_keras_custom_function.sql_in
@@ -0,0 +1,148 @@
+/* -----------------------------------------------------------------------
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * ----------------------------------------------------------------------- */
+
+/* -----------------------------------------------------------------------------
+ * Test load custom function helper functions
+ * -------------------------------------------------------------------------- */
+
+CREATE OR REPLACE FUNCTION custom_function_object()
+RETURNS BYTEA AS
+$$
+import dill
+def test_sum_fn(a, b):
+	return a+b
+
+pb=dill.dumps(test_sum_fn)
+return pb
+$$ language plpythonu;
+
+CREATE OR REPLACE FUNCTION read_custom_function(pb bytea, arg1 int, arg2 int)
+RETURNS INTEGER AS
+$$
+import dill
+obj=dill.loads(pb)
+res=obj(arg1, arg2)
+return res
+$$ language plpythonu;
+
+/* Test successful table creation where no table exists */
+DROP TABLE IF EXISTS test_custom_function_table;
+SELECT load_custom_function('test_custom_function_table', custom_function_object(), 'sum_fn', 'returns sum');
+
+SELECT assert(UPPER(atttypid::regtype::TEXT) = 'INTEGER', 'id column should be INTEGER type')
+    FROM pg_attribute WHERE attrelid = 'test_custom_function_table'::regclass
+        AND attname = 'id';
+SELECT assert(UPPER(atttypid::regtype::TEXT) = 'BYTEA', 'object column should be BYTEA type' )
+    FROM pg_attribute WHERE attrelid = 'test_custom_function_table'::regclass
+        AND attname = 'object';
+SELECT assert(UPPER(atttypid::regtype::TEXT) = 'TEXT',
+    'name column should be TEXT type')
+    FROM pg_attribute WHERE attrelid = 'test_custom_function_table'::regclass
+        AND attname = 'name';
+SELECT assert(UPPER(atttypid::regtype::TEXT) = 'TEXT',
+    'description column should be TEXT type')
+    FROM pg_attribute WHERE attrelid = 'test_custom_function_table'::regclass
+        AND attname = 'description';
+
+/*  id should be 1 */
+SELECT assert(id = 1, 'Wrong id written by load_custom_function')
+    FROM test_custom_function_table;
+
+/* Validate function object created */
+SELECT assert(read_custom_function(object, 2, 3) = 5, 'Custom function should return sum of args.')
+    FROM test_custom_function_table;
+
+/* Test custom function insertion where valid table exists */
+SELECT load_custom_function('test_custom_function_table', custom_function_object(), 'sum_fn1');
+SELECT assert(name = 'sum_fn', 'Custom function sum_fn found in table.')
+    FROM test_custom_function_table WHERE id = 1;
+SELECT assert(name = 'sum_fn1', 'Custom function sum_fn1 found in table.')
+    FROM test_custom_function_table WHERE id = 2;
+
+/* Test adding an existing function name should error out */
+SELECT assert(MADLIB_SCHEMA.trap_error($TRAP$
+SELECT load_custom_function('test_custom_function_table', custom_function_object(), 'sum_fn1');
+$TRAP$) = 1, 'Should error out for duplicate function name');
+
+/* Test deletion by id where valid table exists */
+/* Assert id exists before deleting */
+SELECT assert(COUNT(id) = 1, 'id 2 should exist before deletion!')
+    FROM test_custom_function_table WHERE id = 2;
+SELECT delete_custom_function('test_custom_function_table', 2);
+SELECT assert(COUNT(id) = 0, 'id 2 should have been deleted!')
+    FROM test_custom_function_table WHERE id = 2;
+
+/* Test deletion by name where valid table exists */
+SELECT load_custom_function('test_custom_function_table', custom_function_object(), 'sum_fn1');
+/* Assert id exists before deleting */
+SELECT assert(COUNT(id) = 1, 'function name sum_fn1 should exist before deletion!')
+    FROM test_custom_function_table WHERE name = 'sum_fn1';
+SELECT delete_custom_function('test_custom_function_table', 'sum_fn1');
+SELECT assert(COUNT(id) = 0, 'function name sum_fn1 should have been deleted!')
+    FROM test_custom_function_table WHERE name = 'sum_fn1';
+
+/* Test deleting an already deleted entry should error out */
+SELECT assert(MADLIB_SCHEMA.trap_error($TRAP$
+SELECT delete_custom_function('test_custom_function_table', 2);
+$TRAP$) = 1, 'Should error out for trying to delete an entry that does not exist');
+
+/* Test delete drops the table after deleting last entry*/
+SELECT delete_custom_function('test_custom_function_table', 1);
+SELECT assert(COUNT(relname) = 0, 'Table test_custom_function_table should have been deleted.')
+    FROM pg_class where relname='test_custom_function_table';
+
+/* Test deletion where empty table exists */
+SELECT load_custom_function('test_custom_function_table', custom_function_object(), 'sum_fn', 'returns sum');
+DELETE FROM test_custom_function_table;
+SELECT assert(MADLIB_SCHEMA.trap_error($$SELECT delete_custom_function('test_custom_function_table', 1)$$) = 1,
+    'Deleting function in an empty table should generate an exception.');
+
+/* Test deletion where no table exists */
+DROP TABLE IF EXISTS test_custom_function_table;
+SELECT assert(MADLIB_SCHEMA.trap_error($$SELECT delete_custom_function('test_custom_function_table', 1)$$) = 1,
+              'Deleting a non-existent table should raise exception.');
+
+/* Test where invalid table exists */
+SELECT load_custom_function('test_custom_function_table', custom_function_object(), 'sum_fn', 'returns sum');
+ALTER TABLE test_custom_function_table DROP COLUMN id;
+SELECT assert(MADLIB_SCHEMA.trap_error($$SELECT delete_custom_function('test_custom_function_table', 2)$$) = 1,
+    'Deleting an invalid table should generate an exception.');
+
+SELECT assert(MADLIB_SCHEMA.trap_error($$SELECT load_custom_function('test_custom_function_table', custom_function_object(), 'sum_fn', 'returns sum')$$) = 1,
+    'Passing an invalid table to load_custom_function() should raise exception.');
+
+/* Test input validation */
+DROP TABLE IF EXISTS test_custom_function_table;
+SELECT assert(MADLIB_SCHEMA.trap_error($$
+  SELECT load_custom_function('test_custom_function_table', custom_function_object(), NULL, NULL);
+$$) = 1, 'Name cannot be NULL');
+SELECT assert(MADLIB_SCHEMA.trap_error($$
+  SELECT load_custom_function('test_custom_function_table', NULL, 'sum_fn', NULL);
+$$) = 1, 'Function object cannot be NULL');
+SELECT assert(MADLIB_SCHEMA.trap_error($$
+  SELECT load_custom_function('test_custom_function_table', 'invalid_obj'::bytea, 'sum_fn', NULL);
+$$) = 1, 'Invalid custom function object');
+SELECT load_custom_function('test_custom_function_table', custom_function_object(), 'sum_fn', NULL);
+SELECT assert(name IS NOT NULL AND description IS NULL, 'validate name is not NULL.')
+    FROM test_custom_function_table;
+SELECT assert(MADLIB_SCHEMA.trap_error($$
+  SELECT delete_custom_function('test_custom_function_table', NULL);
+$$) = 1, 'id/name cannot be NULL!');
diff --git a/tool/docker/base/Dockerfile_postgres_10_Jenkins b/tool/docker/base/Dockerfile_postgres_10_Jenkins
index a0d882d..851e499 100644
--- a/tool/docker/base/Dockerfile_postgres_10_Jenkins
+++ b/tool/docker/base/Dockerfile_postgres_10_Jenkins
@@ -33,7 +33,7 @@ RUN apt-get update && apt-get install -y  wget \
                        build-essential \
                        cmake
 
-RUN pip install tensorflow==1.14 keras==2.2.4
+RUN pip install tensorflow==1.14 keras==2.2.4 dill
 
 ## To build an image from this docker file, from madlib folder, run:
 # docker build -t madlib/postgres_10:jenkins -f tool/docker/base/Dockerfile_postgres_10_Jenkins .