Carregamento e execução de funções personalizadas em uma sala limpa

Visão geral

Você pode carregar UDFs e UDTFs Python personalizadas em sua sala limpa e executá-las a partir de seus modelos para realizar ações de dados complexas. Essas ações incluem machine learning ou manipulação de dados personalizados em uma consulta, como parte de uma etapa única ou de um fluxo de várias etapas. Python é a única linguagem de codificação compatível com UDFs personalizadas.

O código carregado pode importar e usar pacotes de um conjunto aprovado de pacotes Python e da Snowpark API.

Templates in a clean room can call code uploaded by the account that added the template. Uploaded code can’t be viewed or downloaded. Snowflake scans uploaded code for security issues before installing the code.

There are different mechanisms for uploading code into a clean room, depending on your role:

Providers

  • Inline code upload: If you want to upload code using the default compute resources for a clean room, and need to use only the standard bundle of Python packages (including the Snowpark API), you should upload inline code.

  • Snowpark Container Services running within a clean room: If you need more control over the environment, such as specifying additional compute or custom libraries, you can run a container within a clean room.

Consumers

  • Inline upload with template: Consumers can upload and run a template bundled with code. The code is bound to the template, and must be approved by the clean room provider.

Este tópico mostra como carregar e executar UDFs e UDTFs do Python personalizados como um provedor ou um consumidor.

Dica

Para obter informações básicas de como desenvolver as próprias UDFs Python em uma sala limpa, consulte os seguintes tópicos:

Entry points for uploaded code

Cada pacote do código carregado pode definir várias funções que chamam umas às outras, mas um pacote expõe apenas uma função do manipulador. Essa função do manipulador pode ser chamada por modelos criados ou executados por qualquer pessoa que use a sala limpa. Se o código criar tabelas internas, essas tabelas poderão ser acessadas conforme descrito em Projeto de fluxos de várias etapas.

Por exemplo, se você carregou uma função chamada simple_add que aceita dois parâmetros numéricos, pode chamá-la a partir de um modelo, conforme mostrado aqui. A função é sempre referenciada usando o escopo cleanroom. Por exemplo, um modelo poderia chamar simple_add assim:

SELECT cleanroom.simple_add({{ price | sqlsafe | int }}, {{ tax | sqlsafe | int }}) ...
Copy

Dica

Se o provedor quiser executar o código acima, ele deverá criar um alias para todas as colunas SELECT que usam uma função agregada ou personalizada, porque uma tabela de resultados é gerada em segundo plano:

SELECT
  cleanroom.simple_add(
    {{ price | sqlsafe | int }}, {{ tax | sqlsafe | int }}
    ) AS TOTAL_ITEM_COST
...
Copy

You can upload multiple functions in a single package, and functions within a single package can call each other, but functions can’t call functions within other packages. (They can call the handler functions, though.) For example, if you have a clean room where you upload two packages, each with a handler function and two helper functions:

Sala limpa com dois pacotes Python enviados

Pacote 1

Pacote 2

  • Função do manipulador A

  • Função auxiliar A1

  • Função auxiliar A2

  • Função do manipulador B

  • Função auxiliar B1

  • Função auxiliar B2

  • Code uploaded by either party (provider or consumer) can be run templates submitted by either party.

  • A template can call function A or function B, but not A1, A2, B1, or B2.

  • A função A pode chamar a função B e vice-versa.

  • A função A não pode chamar B1 ou B2 e a função B não pode chamar A1 ou A2.

  • A1 pode chamar A2 e vice-versa. A1 e A2 podem chamar B. A1 e A2 não podem chamar B1 ou B2.

  • B1 pode chamar B2 e vice-versa. B1 e B2 podem chamar A. B1 e B2 não podem chamar A1 ou A2.

Atualização ou exclusão de funções personalizadas

Você pode carregar ou substituir uma função ou modelo existente, mas não pode excluí-los. A única maneira de «remover» uma função é criar uma função fictícia com exatamente o mesmo nome e assinatura que sempre funciona.

Uploading a function with the same signature as one that you previously uploaded will overwrite the existing function, where a signature means the case-insensitive function name of an external handler, plus the data types of all its arguments, in the same order. Argument names are not part of the signature. You can’t overwrite a function uploaded by another account.

Como a assinatura deve ser igual quando você atualiza uma função, não é possível alterar a assinatura de uma função existente: se você carregar a função foo(name VARIANT age INTEGER) e, em seguida, carregar a função foo(name VARIANT age FLOAT), a segunda função será adicionada à sala limpa junto com a primeira, pois os tipos de argumento são diferentes.

Código enviado pelo provedor

As funções enviadas pelo provedor podem ser carregadas como código em linha ou de uma área de preparação do Snowflake. Ambas as técnicas são abordadas aqui.

O código carregado pode importar e usar pacotes nativamente de um conjunto aprovado de pacotes Python. Se você precisar de um pacote não padrão, use o Snowpark Container Services em uma sala limpa para hospedar o código.

Você não pode visualizar o código do provedor carregado, nem mesmo o seu próprio código; portanto, inclua uma cópia exata do que você carregou em uma sala limpa.

Visão geral

Confira abaixo uma exibição de alto nível de como um provedor adiciona código a uma sala limpa:

  1. O provedor cria e configura a sala limpa como de costume.

  2. The provider uploads a bundle by calling provider.load_python_into_cleanroom. You can either upload your code inline directly within that procedure, or upload a code file to a stage, then provide the stage location to that procedure.

    Although each bundle can include multiple functions, only one handler function is exposed for each upload. To expose multiple functions to templates, upload each handler separately or do a bulk upload (described below).

  3. If the clean room is exposed externally, security checks are run before the code is installed in the clean room, and you must call provider.view_cleanroom_scan_status to confirm that security checks have passed before incrementing the default version.

  4. After each successful upload, a new patch version of the clean room is generated. You must then increase the default version by calling provider.set_default_release_directive with the new patch number.

  5. Create and upload a custom template that calls handlers in your code. The template must call the handler function using the cleanroom scope, that is: cleanroom.my_function(...).

  6. O consumidor executa seu modelo da mesma forma que qualquer outro modelo.

    Dica

    Se o consumidor encontrar um erro de montagem ao instalar uma sala limpa com o código personalizado, talvez seja indicação de erro de sintaxe no código.

Você encontra exemplos de código que demonstram esse fluxo na seção de exemplo de código escrito pelo provedor.

Observações importantes sobre controle de versão

Every time the provider uploads a function, it increases the clean room patch number (and there is a limit of 99 patch numbers). Therefore, do your best to test and debug your code thoroughly before adding it to the clean room to reduce version updates during development.

You can upload multiple packages at once in a single bulk upload to reduce the number of patches generated. However, bulk uploads can make it more challenging to debug if the upload has a security scan issue, because the file that caused the problem isn’t reported in the error response.

Se você atualizar um número de patch, os clientes que usam a UI de salas limpas podem ter que atualizar a página para ver a alteração. Os clientes que usam a API devem ver as mudanças imediatamente, mas pode haver um atraso, dependendo dos recursos disponíveis. Saiba mais sobre o controle de versão de sala limpa.

Carregamento de funções em linha escritas pelo provedor

Você pode carregar o código em linha no parâmetro code de provider.load_python_into_cleanroom. Veja um exemplo de carregamento de uma função simples em linha:

CALL samooha_by_snowflake_local_db.provider.load_python_into_cleanroom(
$cleanroom_name,
'simple_add',                         -- Name used to call the UDF from a template.
['first INTEGER', 'second INTEGER'],  -- Arguments of the UDF, specified as '<variable_name> <SQL type>' pairs.
['numpy', 'pandas'],                  -- Packages imported by the UDF.
'INTEGER',                            -- SQL return type of UDF.
'add_two',                            -- Handler function in your code called when external name is called.
$$
import numpy as np   # Not used, but you can load supported packages.
import pandas as pd

def add_two(first, second):
    return first + second
$$
);
Copy

O modelo que faz a chamada chama cleanroom.simple_add para invocar essa função. Os exemplos do provedor demonstram como carregar um código em linha.

Carregamento de funções escritas pelo provedor de uma área de preparação

You can upload Python files to a clean room stage and reference the stage when you call provider.load_python_into_cleanroom. Loading code from a stage allows you to develop the code in your local system in an editor, avoid copy/paste errors when loading it inline, and also have better versioning control of your source code. Note that you can upload multiple files in a single procedure call, but only one handler function is exposed for each upload.

O código é carregado de uma área de preparação para a sala limpa quando você chama load_python_into_cleanroom. As alterações posteriores feitas no código na área de preparação não serão propagadas à sala limpa.

Para carregar a UDF para uma área de preparação:

  1. Crie seu arquivo .py e disponibilize-o em uma localização onde você possa carregá-lo em uma área de preparação do Snowsight.

  2. To get the name of the stage for your clean room, call provider.get_stage_for_python_files. You must use the specified stage; you cannot use an arbitrary stage that you create.

  3. Carregue o arquivo .py para a área de preparação da sua sala limpa. Há várias maneiras de fazer isso, inclusive usando a CLI, o Snowsight ou drivers específicos da linguagem.

  4. Chame provider.load_python_into_cleanroom com a localização da área de preparação, o manipulador, o nome externo, os argumentos e o tipo de retorno. Os modelos em sua sala limpa agora podem chamar a função.

O código de exemplo a seguir mostra como carregar o código em uma sala limpa de uma área de preparação.

-- Save the following code as reverser.py:
--import numpy as np
--def main(some_string):
--  '''Return the reverse of a string plus a random number 1-10'''
--  return some_string[::-1] + str(np.random.randint(1,10))

-- Get the stage for your clean room.
CALL samooha_by_snowflake_local_db.provider.get_stage_for_python_files($cleanroom_name);

-- Save the file to the stage. Here is how to do it by using the Snowflake CLI
PUT file://~/reverser.py <STAGE_NAME> overwrite=True auto_compress=False;

-- Load the code from the stage into the clean room.
CALL samooha_by_snowflake_local_db.provider.load_python_into_cleanroom(
    $cleanroom_name,
    'reverse', -- Name used to call the function
    ['some_string  STRING'], -- Arguments and SQL types
    ['numpy'],               -- Any required packages
    ['/reverser.py'],        -- Relative path to file on stage
    'STRING',                -- Return type
    'reverser.main'          -- <FILE_NAME>.<FUNCTION_NAME>
);

-- Uploading code, even from a stage, increases the patch number.
CALL samooha_by_snowflake_local_db.provider.set_default_release_directive(
  $cleanroom_name, 'V1_0', <NEW_PATCH_NUMBER>);

-- Upload a template that calls the function.
CALL samooha_by_snowflake_local_db.provider.add_custom_sql_template(
    $cleanroom_name,
    $udf_template_name,
    $$
    SELECT
      p.status,
      cleanroom.reverse(p.status)
    FROM SAMOOHA_SAMPLE_DATABASE.DEMO.CUSTOMERS AS p
    LIMIT 100;
    $$
);

-- Switch to the consumer account and run the template to see the results.
Copy

Os exemplos do provedor demonstram o carregamento do código de uma área de preparação.

Solução de erros de sintaxe ou falhas de verificação no código carregado

Se você carregar uma função que falhe devido a um erro de sintaxe ou se uma verificação de segurança falhar, um patch não publicável poderá ser gerado. Portanto, você deve testar seu código minuciosamente antes do upload para garantir que ele não contenha erros de sintaxe.

Você pode ver a lista de pacotes e os status de revisão deles executando o seguinte comando SQL e fornecendo o ID da sala limpa no local indicado:

SHOW VERSIONS IN APPLICATION PACKAGE samooha_cleanroom_cleanroom_id;

Verificações de segurança

Uma verificação de segurança é executada após qualquer ação que gere uma nova versão de patch em uma sala limpa externa, como quando o provedor carrega o Python na sala limpa (o código enviado pelo consumidor, descrito nesta página, não aciona uma verificação de segurança). As salas limpas internas não executam verificações de segurança, mas se você alterar uma sala limpa interna para uma sala limpa externa, isso acionará uma verificação de segurança para esse patch. Um patch de sala limpa não pode ser publicado externamente até que o patch seja verificado.

O Snowflake Data Clean Rooms usa a estrutura de verificação de segurança do aplicativo Snowflake nativo. Siga as práticas recomendadas de segurança para o aplicativo nativo para evitar erros de verificação de segurança.

Você pode executar ações adicionais de criação de patches antes da conclusão da última verificação de segurança. No entanto, você deve aguardar que provider.view_cleanroom_scan_status mostre sucesso antes de poder atualizar a diretiva de lançamento padrão para servir a versão mais recente da sala limpa.

Uploading multiple Python functions in a single patch (bulk uploading)

If you want to upload multiple Python packages to your clean room, you can call prepare_python_for_cleanroom multiple times, then call load_prepared_python_into_cleanroom once to scan, upload, and generate a single patch for your clean room. The following example demonstrates uploading a UDF and a UDTF using bulk uploading:

---- Add custom inline UDF ----
CALL samooha_by_snowflake_local_db.provider.prepare_python_for_cleanroom(
    $cleanroom_name,
    'get_next_status',  -- Name of the UDF. Can be different from the handler.
    ['status VARCHAR'], -- Arguments of the UDF, specified as (variable name, SQL type).
    ['numpy'],          -- Packages needed by UDF.
    [],                 -- When providing the code inline, this is an empty array.
    'VARCHAR',          -- Return type of UDF.
    'get_next_status',  -- Handler.
    $$
import numpy as np
def get_next_status(status):
  """Return the next higher status, or a random status
  if no matching status found or at the top of the list."""

  statuses = ['MEMBER', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND']
  try:
    return statuses[statuses.index(status.upper()) + 1]
  except:
    return 'NO MATCH'
    $$
);

---- Add custom inline UDTF. ----
CALL samooha_by_snowflake_local_db.provider.prepare_python_for_cleanroom(
    $cleanroom_name,
    'get_info',  -- Name of the UDTF. Can be different from the handler.
    ['hashed_email VARCHAR', 'days_active INT', 'status VARCHAR', 'income VARCHAR'],   -- Name/Type arguments of the UDTF.
    ['numpy'],         -- Packages used by UDTF.
    [],                -- When providing the code inline, this is an empty array.
    'TABLE(hashed_email VARCHAR, months_active INT, level VARCHAR)',  -- Return type of UDTF.
    'GetSomeVals',     -- Handler class name.
$$
class GetSomeVals:
  def __init__(self):
    self.month_days = 30

  def process(self, hashed_email, days_active, status, income):
    '''Change days into rough months, and also return whether we
    think the user's membership status is lower, higher, or equal to
    what is expected, based on their income.'''

    months_active = days_active // self.month_days
    brackets = ['0-50K', '50K-100K', '100K-250K', '250K+']
    statuses = ['MEMBER', 'SILVER', 'GOLD', 'PLATINUM']
    if(statuses.index(status) < brackets.index(income)):
      level = 'low'
    elif(statuses.index(status) > brackets.index(income)):
      level = 'high'
    else:
      level = 'equal'

    yield(hashed_email, months_active, level)
$$
);

-- Upload all stored procedures.
-- Note the new patch number returned by this procedure. Keep this number for later use.
CALL samooha_by_snowflake_local_db.provider.load_prepared_python_into_cleanroom($cleanroom_name);

-- Set the release directive specified by the last load_python_into_cleanroom call.
CALL samooha_by_snowflake_local_db.provider.set_default_release_directive($cleanroom_name, 'V1_0', <PATCH_NUMBER>);
Copy

Exemplos de código escrito pelo provedor

Os exemplos a seguir demonstram como adicionar UDFs e UDTFs escritas pelo provedor a uma sala limpa.

Download the following examples and then upload them as worksheet files in your Snowflake account. You need separate accounts for the provider and consumer, each with the clean rooms API installed. Replace the information as noted in the sample files. See instructions to upload a SQL worksheet into your Snowflake account.

Código enviado pelo consumidor

O código carregado pelo consumidor é agrupado e carregado com um modelo personalizado usando o fluxo de upload de modelo do consumidor. O código carregado pode ser chamado por qualquer modelo na sala limpa.

Para fazer upload de código como consumidor, você deve compreender a sintaxe de modelo personalizado.

Observe que qualquer código carregado por um consumidor pode ser visto pelo provedor quando ele solicita permissão de carregamento. O código do consumidor também fica visível sempre que um provedor ou consumidor examina o modelo.

Confira abaixo uma visão geral das etapas para carregar o código do consumidor personalizado:

  1. O provedor cria a sala limpa como de costume e convida o consumidor.

  2. O consumidor instala e configura a sala limpa como de costume.

  3. O consumidor prepara um modelo que chama UDF ou UDTF dentro do namespace cleanroom. Por exemplo, para chamar a função calculate_tax definida pelo consumidor, um modelo simples pode parecer com o seguinte trecho:

    SELECT {{ cleanroom.calculate_tax(p.cost) }} AS Tax FROM my_db.my_sch.sales AS p;
    
    Copy
  4. O consumidor prepara o código Python. Recomendamos o uso de aspas duplas (" ") em vez de aspas simples (' ') no código para evitar ter que inserir um caractere de escape extra no futuro. Seu código pode fazer referência a estas bibliotecas Python aceitas.

  5. O consumidor passa o código Python para consumer.generate_python_request_template. O procedimento retorna o código Python como um procedimento armazenado, com um espaço reservado para o modelo JinjaSQL personalizado. Há várias cadeias de caracteres multilinha no modelo que usam $$ como delimitadores de várias linhas.

  6. O consumidor substitui o espaço reservado para o modelo na saída de generate_python_request_template pelo modelo JinjaSQL.

  7. No modelo combinado, insira um caractere de escape para aspas simples como esta: \'. Isso ocorre porque aspas simples serão usadas como o delimitador mais externo para toda a cadeia de caracteres do procedimento multilinha quando você enviá-la para a sala limpa. Aqui está um exemplo de um procedimento armazenado que inclui o código Python do consumidor e o modelo personalizado, com escape de caracteres:

      BEGIN
    
      CREATE OR REPLACE FUNCTION CLEANROOM.custom_compare(min_status STRING, max_status STRING, this_status STRING)
      RETURNS boolean
      LANGUAGE PYTHON
      RUNTIME_VERSION = 3.10
      PACKAGES = (\'numpy\')
    
      HANDLER = \'custom_compare\'
      AS $$
      import numpy as np
    
      def custom_compare(min_status:str, max_status:str, this_status:str):
        statuses = [\'MEMBER\', \'SILVER\', \'GOLD\', \'PLATINUM\']
        return ((statuses.index(this_status) >= statuses.index(min_status)) &
                (statuses.index(this_status) <= statuses.index(max_status)))
      $$;
    
      -- Custom template
      LET SQL_TEXT varchar := $$
      SELECT
        c.status,
        c.hashed_email
      FROM IDENTIFIER( {{ my_table[0] }} ) as c
      WHERE cleanroom.custom_compare({{ min_status }}, {{ max_status }}, c.status);
      $$;
    
      LET RES resultset := (EXECUTE IMMEDIATE :SQL_TEXT);
      RETURN TABLE(RES);
    
      END;
    
    Copy
  8. O consumidor chama consumer.create_template_request com o modelo combinado. Coloque o código que você fornece para o procedimento armazenado entre aspas simples (' '), em vez dos delimitadores de cifrões duplos ($$...$$), no argumento template_definition. Por exemplo:

    CALL samooha_by_snowflake_local_db.consumer.create_template_request(
      $cleanroom_name,
      $template_name,
      '
    BEGIN
    
    -- First, define the Python UDF.
    CREATE OR REPLACE FUNCTION CLEANROOM.custom_compare(min_status STRING, max_status STRING, this_status STRING)
    RETURNS boolean
    LANGUAGE PYTHON
    RUNTIME_VERSION = 3.10
    PACKAGES = (\'numpy\')
    
    HANDLER = \'custom_compare\'
    AS $$
    import numpy as np
    
    def custom_compare(min_status:str, max_status:str, this_status:str):
      statuses = [\'MEMBER\', \'SILVER\', \'GOLD\', \'PLATINUM\']
      return ((statuses.index(this_status) >= statuses.index(min_status)) &
              (statuses.index(this_status) <= statuses.index(max_status)))
        $$;
    
    -- Then define and execute the SQL query.
    LET SQL_TEXT varchar := $$
    SELECT
      c.status,
      c.hashed_email
    FROM IDENTIFIER( {{ my_table[0] }} ) as c
    WHERE cleanroom.custom_compare({{ min_status }}, {{ max_status }}, c.status);
    $$;
    
    -- Execute the query and then return the result.
    LET RES resultset := (EXECUTE IMMEDIATE :SQL_TEXT);
    RETURN TABLE(RES);
    
    END;
    ');
    
    Copy
  9. O consumidor e o provedor continuam com o fluxo do modelo definido pelo consumidor padrão:

    1. O provedor visualiza a solicitação de modelo (provider.list_pending_template_requests) e chama approve_template_request para aprová-la. Na solicitação, o provedor pode ver o modelo e o código no pacote.

    2. O consumidor verifica o status da solicitação (consumer.list_template_requests) e, quando o status é APPROVED, executa o modelo (consumer.run_analysis).

    Consumer code uploads don’t trigger a security scan or affect the clean room patch number.

Exemplos de código escrito pelo consumidor

Os exemplos a seguir demonstram como adicionar UDFs escritas pelo provedor a uma sala limpa.

Download the following examples and then upload them as worksheet files in your Snowflake account. You need separate accounts for the provider and consumer, each with the clean rooms API installed. Replace the information as noted in the sample files. See instructions to upload a SQL worksheet into your Snowflake account.