> For the complete documentation index, see [llms.txt](https://docs.seemoredata.io/external-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.seemoredata.io/external-docs/fundamentals/getting-set-up/setting-integrations/snowflake/create-snowflake-user-and-privileges.md).

# Create Snowflake User and Privileges

This page walks you through creating the Snowflake role, user, and privileges that SeeMore Data needs to connect to your account.

{% hint style="info" %}
**Two ways to set this up:**

* **Step by step** — follow the sections below to create and grant each piece individually. The [Core Setup Script](#comprehensive-setup-script) combines the essential steps into a single block.
* **All at once** — to enable every capability (core plus all optional add-ons) in one run, jump to the [Complete Setup Script (All Permissions)](#complete-setup-script) at the end of this page.
  {% endhint %}

**What you'll set up on this page:**

1. **Core integration** — a role, a user (key-pair authentication), and the privileges SeeMore Data needs to read metadata and monitor usage.
2. **Optional capabilities** — extra procedures and grants that unlock specific features: streams & dynamic-table monitoring, auto-clustering recommendations, and Cortex Code cost controls.
3. **Account-level options** — enabling `ORGADMIN` for organization-wide observability, and allowlisting the SeeMore Data IPs.

### Create a role <a href="#create-a-user-0-1" id="create-a-user-0-1"></a>

Create a role in Snowflake using the following commands:

{% code lineNumbers="true" %}

```sql
-- Switch to the ACCOUNTADMIN role to perform administrative tasks such as creating warehouses.
USE ROLE ACCOUNTADMIN;
CREATE ROLE IF NOT EXISTS seemore_user_role;
GRANT OPERATE, USAGE ON WAREHOUSE "<warehouse_name>" TO ROLE seemore_user_role;
```

{% endcode %}

* Replace `<warehouse_name>` with the default warehouse to use when running the Seemore snowflake "metadata fetcher".

### Create a user <a href="#create-a-user-0-1" id="create-a-user-0-1"></a>

SeeMore Data connects to Snowflake using **key-pair authentication**. You'll generate an RSA key pair, create the user with the public key, and later provide the private key to SeeMore Data when you set up the integration.

{% hint style="info" %}
Key-pair authentication is the recommended and supported method. It avoids storing a password on the connection and isn't affected by password-rotation policies.
{% endhint %}

#### 1. Generate the private key

Open a terminal window and generate a private key. You can generate either an encrypted or an unencrypted version.

To generate an **unencrypted** version, use:

```bash
openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out rsa_key.p8 -nocrypt
```

To generate an **encrypted** version, use the following command, which omits `-nocrypt`:

```bash
openssl genrsa 2048 | openssl pkcs8 -topk8 -v2 des3 -inform PEM -out rsa_key.p8
```

#### 2. Generate the public key

```bash
openssl rsa -in rsa_key.p8 -pubout -out rsa_key.pub
```

{% hint style="danger" %} <mark style="color:$danger;">**Store the private and public keys securely !!!**</mark>
{% endhint %}

#### 3. Create the user with the public key

Create the user and assign the public key you just generated. Replace `<public_key>` with the contents of `rsa_key.pub` **without** the `-----BEGIN PUBLIC KEY-----` / `-----END PUBLIC KEY-----` lines, and replace `<warehouse_name>` with your warehouse.

{% code overflow="wrap" lineNumbers="true" %}

```sql
CREATE USER IF NOT EXISTS seemore_user
    RSA_PUBLIC_KEY='<public_key>'
    DEFAULT_ROLE=seemore_user_role
    DEFAULT_WAREHOUSE='<warehouse_name>'
    DISPLAY_NAME='SeemoreData';
```

{% endcode %}

If the user already exists, attach the key instead with:

```sql
ALTER USER seemore_user SET RSA_PUBLIC_KEY='<public_key>';
```

{% hint style="warning" %}
When providing the private key to SeeMore Data, include the full PEM format **with the header and footer lines**:

```
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFJDBWBgkqhkiG9w0BBQ0wSTAxBgkqhkiG9w0BBQwwJAQQ/rZQBkrrC7xzJPDV...
-----END ENCRYPTED PRIVATE KEY-----
```

For unencrypted keys the header/footer will be `-----BEGIN PRIVATE KEY-----` / `-----END PRIVATE KEY-----`.
{% endhint %}

For more details, see the official Snowflake [key-pair authentication documentation](https://docs.snowflake.com/en/user-guide/key-pair-auth).

### Grant role to user <a href="#grant-role-to-user-0-5" id="grant-role-to-user-0-5"></a>

To grant the `user_role` to the new user:

{% code lineNumbers="true" %}

```sql
GRANT ROLE seemore_user_role TO USER seemore_user;
```

{% endcode %}

### Grant Privileges <a href="#choose-metadata-fetching-method-0-6" id="choose-metadata-fetching-method-0-6"></a>

In order to fetch metadata from snowflake,\
Seemore needs access only to SNOWFLAKE using GRANT IMPORTED PRIVILEGES.

{% code lineNumbers="true" %}

```sql
-- Grant the role permission to import metadata from the Snowflake database.
GRANT IMPORTED PRIVILEGES ON DATABASE SNOWFLAKE TO ROLE seemore_user_role;

-- Grant the role permission to monitor usage on the entire account.
GRANT MONITOR USAGE ON ACCOUNT TO ROLE seemore_user_role;
```

{% endcode %}

### Core Setup Script <a href="#comprehensive-setup-script" id="comprehensive-setup-script"></a>

This script covers the **core integration** — creating a dedicated warehouse for Seemore, establishing roles, creating the user with key-pair authentication, and assigning the necessary privileges for operations and monitoring. Replace `<public_key>` with the contents of your `rsa_key.pub` (without the `-----BEGIN/END PUBLIC KEY-----` lines) — see [Create a user](#create-a-user-0-1) for how to generate the key pair.\
If you don't need a dedicated warehouse, comment out the `CREATE WAREHOUSE` block and replace `seemore_warehouse` with your own warehouse name.

{% hint style="info" %}
This is enough to connect SeeMore Data and start monitoring. It does **not** include the optional capabilities further down this page. To provision the core integration **and** every optional add-on in a single run, use the [Complete Setup Script (All Permissions)](#complete-setup-script) at the end of this page.
{% endhint %}

{% code lineNumbers="true" expandable="true" %}

```sql
-- Switch to the ACCOUNTADMIN role to perform administrative tasks such as creating warehouses.
USE ROLE ACCOUNTADMIN;

-- Optionally create a new warehouse named "seemore_warehouse" if it does not already exist.
-- The warehouse is set to 'XSMALL' size, with auto-suspension after 60 seconds of inactivity.
-- It's initially suspended to avoid unnecessary costs. A comment is added for clarity.
CREATE WAREHOUSE IF NOT EXISTS seemore_warehouse
    WAREHOUSE_SIZE='XSMALL'
    AUTO_SUSPEND=60
    INITIALLY_SUSPENDED=TRUE
    COMMENT = 'Used by seemore';
           
-- Create a new role for the seemore user, only if it doesn't exist.
CREATE ROLE IF NOT EXISTS seemore_user_role;

-- Grant the new role permissions to operate and use the "seemore_warehouse".
GRANT OPERATE, USAGE ON WAREHOUSE seemore_warehouse TO ROLE seemore_user_role;


-- Create a new user named "seemore_user" only if it doesn't exist.
-- The public key is assigned for key-pair authentication; its default role
-- and warehouse are set to "seemore_user_role" and "seemore_warehouse".
CREATE USER IF NOT EXISTS seemore_user 
    RSA_PUBLIC_KEY = '<public_key>'
    DEFAULT_ROLE = seemore_user_role 
    DEFAULT_WAREHOUSE = 'seemore_warehouse' 
    DISPLAY_NAME = 'SeemoreData';

-- Grant the previously created role to the seemore user.
GRANT ROLE seemore_user_role TO USER seemore_user;

-- Grant the role permission to import metadata from the Snowflake database.
GRANT IMPORTED PRIVILEGES ON DATABASE SNOWFLAKE TO ROLE seemore_user_role;

-- Grant the role permission to monitor usage on the entire account.
GRANT MONITOR USAGE ON ACCOUNT TO ROLE seemore_user_role;

-- Grant the ability to monitor warehouse settings through the manage warehouses permission
-- This permission is also required for Seemore to retrieve query statistics using get_query_operator_stats, and for the smart pulse, auto-shutdown, auto-scaler
GRANT MANAGE WAREHOUSES ON ACCOUNT TO ROLE seemore_user_role;
```

{% endcode %}

### Monitoring Additional Snowflake Services <a href="#allowlist-the-datamaze-ip-0-15" id="allowlist-the-datamaze-ip-0-15"></a>

#### Tune Warehouses Automations

To allow Seemore to analyze, optimize, and implement the best configurations for your warehouses, please grant the necessary permissions. These permissions are also needed for Seemore to access query statistics via `get_query_operator_stats` and to perform smart pulse, auto-shutdown, and auto-scaling operations.

```sql
-- Grant the ability to monitor warehouse settings through the manage warehouses permission
-- This permission is also required for Seemore to retrieve query statistics using get_query_operator_stats, and for the smart pulse, auto-shutdown, auto-scaler
GRANT MANAGE WAREHOUSES ON ACCOUNT TO ROLE seemore_user_role;
```

#### Streams, Dynamic tables & Materialized Views <a href="#allowlist-the-datamaze-ip-0-15" id="allowlist-the-datamaze-ip-0-15"></a>

* **Create (or use) a dedicated database and schema** to house the procedure:

  ```sql
  CREATE DATABASE IF NOT EXISTS seemore;
  CREATE SCHEMA IF NOT EXISTS seemore.utils;
  ```
* **Grant the necessary privileges to `seemore_user_role`** so it can execute the procedure and view the results:

  ```sql
  GRANT USAGE ON DATABASE seemore TO ROLE seemore_user_role;
  GRANT USAGE ON SCHEMA seemore.utils TO ROLE seemore_user_role;
  ```

  *(Adjust privileges if additional permissions are required by your security model.)*
* **Use a role with \`ACCOUNTADMIN\` to create or replace the procedure.**\
  This high-level role is needed to grant the procedure enough access to list all streams.
* **Create (or replace) the stored procedure** with **`EXECUTE AS OWNER`**. This setting ensures the procedure runs with the privileges of the owner (the role that created it). That way, even if the caller’s role does not have the privileges to see all streams directly, the procedure can still return the entire list:

{% code expandable="true" %}

```sql
CREATE OR REPLACE PROCEDURE seemore.utils.list_object(OBJECT_KEY VARCHAR)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
    var sql_command;
    var key = OBJECT_KEY.toLowerCase();

    if (key === 'streams') {
        sql_command = 'SHOW STREAMS IN ACCOUNT';
    } else if (key === 'dynamic_tables') {
        sql_command = 'SHOW DYNAMIC TABLES IN ACCOUNT';
    } else if (key === 'materialized_views') {
        sql_command = 'SHOW MATERIALIZED VIEWS IN ACCOUNT';
    } else {
        throw 'Invalid object type. Use one of: "streams", "dynamic_tables", "materialized_views"';
    }

    var stmt = snowflake.createStatement({ sqlText: sql_command });
    var result = stmt.execute();

    var objectsList = [];
    while (result.next()) {
        var rowObj = {};
        for (var i = 1; i <= result.getColumnCount(); i++) {
            var colName = result.getColumnName(i);
            var colValue = result.getColumnValue(i);
            rowObj[colName] = colValue;
        }
        objectsList.push(rowObj);
    }

    return objectsList;
$$
;

GRANT USAGE ON PROCEDURE seemore.utils.list_object(VARCHAR) TO ROLE seemore_user_role;
```

{% endcode %}

### Auto clustering recommendations <a href="#allowlist-the-datamaze-ip-0-15" id="allowlist-the-datamaze-ip-0-15"></a>

#### Permissions to Auto Clustering monitoring and recommendations <a href="#allowlist-the-datamaze-ip-0-15" id="allowlist-the-datamaze-ip-0-15"></a>

By creating the below procedures and give Seemore the access to it we will be able to help you cluster your tables in a more efficient way and also improve Snowflake's auto-clustering feature,\
Without the need of exposing data at all.

{% hint style="success" %}
**This configuration grants access to table metadata only, ensuring your actual data remains private and inaccessible**
{% endhint %}

* First, create the Seemore database and schema if not exists:

```sql
CREATE DATABASE IF NOT EXISTS seemore;
CREATE SCHEMA IF NOT EXISTS seemore.utils;

GRANT USAGE ON DATABASE seemore TO ROLE seemore_user_role;
GRANT USAGE ON SCHEMA seemore.utils TO ROLE seemore_user_role;
```

* **It is important to create the following procedures using \`ACCOUNTADMIN\` role.**\
  This high-level role is needed to grant the procedure access to all the tables clustering information.
* **GET\_CLUSTERING\_INFO** -\
  Create a procedure that gives Seemore an ability to access [snowflake clustering information method](https://docs.snowflake.com/en/sql-reference/functions/system_clustering_information), which allows us optimizing our recommendation of clustering columns candidates,\
  based on actual cardinality stats of the tables.

{% code expandable="true" %}

```sql
CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.GET_CLUSTERING_INFO(
  table_fqn STRING,
  column_combinations ARRAY
)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  // Validate inputs
  if (!TABLE_FQN || typeof TABLE_FQN !== 'string' || TABLE_FQN.trim() === '') {
    throw new Error('table_fqn must be a non-empty string');
  }
  
  if (!COLUMN_COMBINATIONS || !Array.isArray(COLUMN_COMBINATIONS) || COLUMN_COMBINATIONS.length === 0) {
    throw new Error('column_combinations must be a non-empty array');
  }
  
  const results = [];
  
  // Process each column combination
  for (let i = 0; i < COLUMN_COMBINATIONS.length; i++) {
    const combination = COLUMN_COMBINATIONS[i];
    
    // Validate combination is an array
    if (!Array.isArray(combination) || combination.length === 0) {
      // Skip invalid combinations but log them
      results.push({
        combination: [],
        error: 'Invalid combination: must be a non-empty array',
        metrics: null
      });
      continue;
    }
    
    try {
      // Escape column names for SQL (handle quotes and special characters)
      const escapedColumns = combination.map(col => {
        if (typeof col !== 'string') {
          throw new Error(`Column name must be a string, got: ${typeof col}`);
        }
        return col;
      });
      
      // Build column list string for SYSTEM$CLUSTERING_INFORMATION
      // Format: (col1, col2, col3)
      const columnList = '(' + escapedColumns.join(', ') + ')';
      
      // Build the SQL statement - use parameterized query to prevent SQL injection
      const sql = `SELECT SYSTEM$CLUSTERING_INFORMATION('${TABLE_FQN}', '${columnList}') AS clustering_info`;
      
      // Execute the statement
      const stmt = snowflake.createStatement({ sqlText: sql });
      const resultSet = stmt.execute();
      
      if (resultSet.next()) {
        const clusteringInfo = resultSet.getColumnValue(1);
        
        // Parse the JSON response from SYSTEM$CLUSTERING_INFORMATION
        let metrics;
        if (typeof clusteringInfo === 'string') {
          try {
            metrics = JSON.parse(clusteringInfo);
          } catch (parseErr) {
            throw new Error(`Failed to parse clustering info JSON: ${parseErr.message}`);
          }
        } else if (clusteringInfo && typeof clusteringInfo === 'object') {
          metrics = clusteringInfo;
        } else {
          throw new Error(`Unexpected clustering info format: ${typeof clusteringInfo}`);
        }
        
        // Build result object with all relevant metrics
        results.push({
          combination: combination,
          metrics: {
            average_depth: metrics.average_depth !== undefined ? metrics.average_depth : null,
            average_overlaps: metrics.average_overlaps !== undefined ? metrics.average_overlaps : null,
            total_partition_count: metrics.total_partition_count !== undefined ? metrics.total_partition_count : null,
            total_constant_partition_count: metrics.total_constant_partition_count !== undefined ? metrics.total_constant_partition_count : null,
            cluster_by_keys: metrics.cluster_by_keys || null,
            notes: metrics.notes || null,
            partition_depth_histogram: metrics.partition_depth_histogram || null
          }
        });
      } else {
        // No result returned
        results.push({
          combination: combination,
          error: 'No result returned from SYSTEM$CLUSTERING_INFORMATION',
          metrics: null
        });
      }
    } catch (err) {
      // Log error but continue with other combinations
      const errorMessage = err.message || String(err);
      results.push({
        combination: combination,
        error: errorMessage,
        metrics: null
      });
    }
  }
  
  // Return results as VARIANT (automatically converted from JavaScript array/object)
  return results;
$$;

GRANT USAGE ON PROCEDURE SEEMORE.UTILS.GET_CLUSTERING_INFO(VARCHAR, ARRAY) TO ROLE seemore_user_role;
```

{% endcode %}

* **GET\_CLUSTERING\_COST\_ESTIMATE**\
  Create a procedure that gives Seemore an access to [auto clustering cost estimate method](https://docs.snowflake.com/en/sql-reference/functions/system_estimate_automatic_clustering_costs),\
  So Seemore can use it for estimating the cost of turning on auto clustering based on out recommendations.

{% code expandable="true" %}

```sql
CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.GET_CLUSTERING_COST_ESTIMATE(
  table_fqn STRING,  
  column_combination ARRAY
)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  // Validate inputs
  if (!TABLE_FQN || typeof TABLE_FQN !== 'string' || TABLE_FQN.trim() === '') {
    throw new Error('table_fqn must be a non-empty string');
  }
  
  if (!COLUMN_COMBINATION || !Array.isArray(COLUMN_COMBINATION) || COLUMN_COMBINATION.length === 0) {
    throw new Error('column_combination must be a non-empty array');
  }
  
  try {
    // Escape column names for SQL (handle quotes and special characters)
    const escapedColumns = COLUMN_COMBINATION.map(col => {
      if (typeof col !== 'string') {
        throw new Error(`Column name must be a string, got: ${typeof col}`);
      }
      // Escape double quotes by doubling them
      const escaped = col.replace(/"/g, '""');
      return `"${escaped}"`;
    });
    
    // Build column list string for SYSTEM$ESTIMATE_AUTOMATIC_CLUSTERING_COSTS
    // Format: (col1, col2, col3)
    const columnList = '(' + escapedColumns.join(', ') + ')';
    
    // Build the SQL statement
    const sql = `SELECT SYSTEM$ESTIMATE_AUTOMATIC_CLUSTERING_COSTS('${TABLE_FQN}', '${columnList}') AS cost_estimation`;
    
    // Execute the statement
    const stmt = snowflake.createStatement({ sqlText: sql });
    const resultSet = stmt.execute();
    
    if (resultSet.next()) {
      const costEstimationJson = resultSet.getColumnValue(1);
      
      // Parse the JSON response from SYSTEM$ESTIMATE_AUTOMATIC_CLUSTERING_COSTS
      let costEstimation;
      if (typeof costEstimationJson === 'string') {
        try {
          costEstimation = JSON.parse(costEstimationJson);
        } catch (parseErr) {
          throw new Error(`Failed to parse cost estimation JSON: ${parseErr.message}`);
        }
      } else if (costEstimationJson && typeof costEstimationJson === 'object') {
        costEstimation = costEstimationJson;
      } else {
        throw new Error(`Unexpected cost estimation format: ${typeof costEstimationJson}`);
      }
      
      // Return result object with combination and costs
      return {
        combination: COLUMN_COMBINATION,
        costs: {
          initial: costEstimation.initial !== undefined ? costEstimation.initial : null,
          maintenance: costEstimation.maintenance !== undefined ? costEstimation.maintenance : null
        }
      };
    } else {
      // No result returned
      throw new Error('No result returned from SYSTEM$ESTIMATE_AUTOMATIC_CLUSTERING_COSTS');
    }
  } catch (err) {
    // Return error information
    const errorMessage = err.message || String(err);
    return {
      combination: COLUMN_COMBINATION,
      error: errorMessage,
      costs: null
    };
  }
$$;


GRANT USAGE ON PROCEDURE SEEMORE.UTILS.GET_CLUSTERING_COST_ESTIMATE(VARCHAR, ARRAY) TO ROLE seemore_user_role;

```

{% endcode %}

### Cortex Code Cost Controls <a href="#cortex-code-cost-controls" id="cortex-code-cost-controls"></a>

Snowflake's [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/credit-usage-limit) is an AI assistant available in Snowsight and the Snowflake CLI. Each interaction consumes credits, and without limits, usage can grow quickly. Seemore can help you manage Cortex Code costs by setting daily estimated credit usage limits per user across both surfaces (CLI and Snowsight).

Limits are tracked over a **rolling 24-hour window**. When a user's estimated usage reaches the configured threshold, access is blocked for that surface until usage drops below the limit.

| Limit value     | Behavior                                                         |
| --------------- | ---------------------------------------------------------------- |
| -1 (default)    | No limit — unlimited access                                      |
| 0               | Access is blocked entirely                                       |
| Positive number | Access is blocked when estimated credit usage exceeds this value |

Limits can be applied at the **account level** (all users) or at the **user level** (overrides the account-level setting for that user).

{% hint style="info" %}
The Seemore database and schema must already exist. If you haven't created them yet, refer to the setup in the [Streams, Dynamic tables & Materialized Views](#allowlist-the-datamaze-ip-0-15) section above.
{% endhint %}

* **It is important to create the following procedures using `ACCOUNTADMIN` role.**\
  This high-level role is needed because `ALTER ACCOUNT` and `ALTER USER` parameter changes require elevated privileges.

The script below creates four procedures:

* **SET\_CORTEX\_CODE\_LIMIT** — Set a daily estimated credit usage limit at the account or user level.
* **UNSET\_CORTEX\_CODE\_LIMIT** — Remove a previously set limit, restoring the default (unlimited) behavior.
* **GET\_CORTEX\_CODE\_ACCOUNT\_LIMITS** — Retrieve the current Cortex Code credit limits at the account level.
* **GET\_CORTEX\_CODE\_USER\_LIMITS** — Retrieve the current Cortex Code credit limits for a specific user.

{% code expandable="true" %}

```sql
-- ============================================================
-- Cortex Code Cost Controls — Stored Procedures for Seemore
-- Run as ACCOUNTADMIN
-- ============================================================

USE ROLE ACCOUNTADMIN;

-- 1. SET_CORTEX_CODE_LIMIT
-- Sets a daily estimated credit usage limit for Cortex Code
-- at the account or user level.
CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.SET_CORTEX_CODE_LIMIT(
  P_TARGET STRING,
  P_USERNAME STRING,
  P_PARAMETER_NAME STRING,
  P_VALUE STRING
)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  var ALLOWED_PARAMS = [
    'CORTEX_CODE_CLI_DAILY_EST_CREDIT_LIMIT_PER_USER',
    'CORTEX_CODE_SNOWSIGHT_DAILY_EST_CREDIT_LIMIT_PER_USER',
    'CORTEX_CODE_DESKTOP_DAILY_EST_CREDIT_LIMIT_PER_USER'
  ];

  if (!P_PARAMETER_NAME || ALLOWED_PARAMS.indexOf(P_PARAMETER_NAME) === -1) {
    throw new Error('Invalid parameter name: ' + P_PARAMETER_NAME +
      '. Allowed: ' + ALLOWED_PARAMS.join(', '));
  }

  var numericValue = parseInt(P_VALUE, 10);
  if (isNaN(numericValue)) {
    throw new Error('P_VALUE must be a valid integer, got: ' + P_VALUE);
  }

  var target = (P_TARGET || '').toLowerCase();
  if (target !== 'account' && target !== 'user') {
    throw new Error('P_TARGET must be "account" or "user"');
  }

  var sql;
  if (target === 'account') {
    sql = 'ALTER ACCOUNT SET ' + P_PARAMETER_NAME + ' = ' + numericValue;
  } else {
    if (!P_USERNAME || typeof P_USERNAME !== 'string' || P_USERNAME.trim() === '') {
      throw new Error('P_USERNAME is required when target is "user"');
    }
    var escapedUsername = P_USERNAME.replace(/"/g, '""');
    sql = 'ALTER USER "' + escapedUsername + '" SET ' + P_PARAMETER_NAME + ' = ' + numericValue;
  }

  var stmt = snowflake.createStatement({ sqlText: sql });
  stmt.execute();

  return { status: 'success', target: target, parameter: P_PARAMETER_NAME, value: numericValue };
$$;

GRANT USAGE ON PROCEDURE SEEMORE.UTILS.SET_CORTEX_CODE_LIMIT(STRING, STRING, STRING, STRING) TO ROLE seemore_user_role;


-- 2. UNSET_CORTEX_CODE_LIMIT
-- Removes a previously set credit limit, restoring the default (unlimited).
CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.UNSET_CORTEX_CODE_LIMIT(
  P_TARGET STRING,
  P_USERNAME STRING,
  P_PARAMETER_NAME STRING
)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  var ALLOWED_PARAMS = [
    'CORTEX_CODE_CLI_DAILY_EST_CREDIT_LIMIT_PER_USER',
    'CORTEX_CODE_SNOWSIGHT_DAILY_EST_CREDIT_LIMIT_PER_USER',
    'CORTEX_CODE_DESKTOP_DAILY_EST_CREDIT_LIMIT_PER_USER'
  ];

  if (!P_PARAMETER_NAME || ALLOWED_PARAMS.indexOf(P_PARAMETER_NAME) === -1) {
    throw new Error('Invalid parameter name: ' + P_PARAMETER_NAME +
      '. Allowed: ' + ALLOWED_PARAMS.join(', '));
  }

  var target = (P_TARGET || '').toLowerCase();
  if (target !== 'account' && target !== 'user') {
    throw new Error('P_TARGET must be "account" or "user"');
  }

  var sql;
  if (target === 'account') {
    sql = 'ALTER ACCOUNT UNSET ' + P_PARAMETER_NAME;
  } else {
    if (!P_USERNAME || typeof P_USERNAME !== 'string' || P_USERNAME.trim() === '') {
      throw new Error('P_USERNAME is required when target is "user"');
    }
    var escapedUsername = P_USERNAME.replace(/"/g, '""');
    sql = 'ALTER USER "' + escapedUsername + '" UNSET ' + P_PARAMETER_NAME;
  }

  var stmt = snowflake.createStatement({ sqlText: sql });
  stmt.execute();

  return { status: 'success', target: target, parameter: P_PARAMETER_NAME };
$$;

GRANT USAGE ON PROCEDURE SEEMORE.UTILS.UNSET_CORTEX_CODE_LIMIT(STRING, STRING, STRING) TO ROLE seemore_user_role;


-- 3. GET_CORTEX_CODE_ACCOUNT_LIMITS
-- Retrieves current Cortex Code credit limits at the account level.
CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.GET_CORTEX_CODE_ACCOUNT_LIMITS()
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  var stmt = snowflake.createStatement({ sqlText: "SHOW PARAMETERS LIKE 'CORTEX_CODE_%' IN ACCOUNT" });
  var result = stmt.execute();

  var rows = [];
  while (result.next()) {
    var row = {};
    for (var i = 1; i <= result.getColumnCount(); i++) {
      row[result.getColumnName(i)] = result.getColumnValue(i);
    }
    rows.push(row);
  }

  // Version marker: lets Seemore detect that these utilities support the Desktop
  // channel. Bump this whenever the set of managed parameters changes.
  rows.push({ key: 'SEEMORE_CORTEX_CODE_UTILS_VERSION', value: '2', level: 'ACCOUNT', type: 'NUMBER' });

  return rows;
$$;

GRANT USAGE ON PROCEDURE SEEMORE.UTILS.GET_CORTEX_CODE_ACCOUNT_LIMITS() TO ROLE seemore_user_role;


-- 4. GET_CORTEX_CODE_USER_LIMITS
-- Retrieves current Cortex Code credit limits for a specific user.
CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.GET_CORTEX_CODE_USER_LIMITS(
  P_USERNAME STRING
)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  if (!P_USERNAME || typeof P_USERNAME !== 'string' || P_USERNAME.trim() === '') {
    throw new Error('P_USERNAME must be a non-empty string');
  }

  var escapedUsername = P_USERNAME.replace(/"/g, '""');
  var sql = "SHOW PARAMETERS LIKE 'CORTEX_CODE_%' IN USER \"" + escapedUsername + "\"";

  var stmt = snowflake.createStatement({ sqlText: sql });
  var result = stmt.execute();

  var rows = [];
  while (result.next()) {
    var row = {};
    for (var i = 1; i <= result.getColumnCount(); i++) {
      row[result.getColumnName(i)] = result.getColumnValue(i);
    }
    rows.push(row);
  }

  return rows;
$$;

GRANT USAGE ON PROCEDURE SEEMORE.UTILS.GET_CORTEX_CODE_USER_LIMITS(STRING) TO ROLE seemore_user_role;
```

{% endcode %}

{% hint style="success" %}
**These procedures only modify Cortex Code credit limit parameters — they do not access any user data or query history.**
{% endhint %}

### Enable ORGADMIN <a href="#enable-orgadmin" id="enable-orgadmin"></a>

To unlock **organization-level observability** and **contract observability** in Seemore, the Snowflake account must be attached to the `ORGADMIN` role.

1. Log in to your Snowflake organization account with a user that has the `ORGADMIN` role.
2. Navigate to **Admin → Accounts**.
3. Click the **`...`** menu on the account you want to connect to Seemore.
4. Select **Enable ORGADMIN**.

<figure><img src="/files/hwqu0IRL7z5HtFfIdYgq" alt="Enable ORGADMIN on a Snowflake account"><figcaption><p>Enable ORGADMIN from the Accounts page</p></figcaption></figure>

Alternatively, you can enable it via SQL:

```sql
USE ROLE ORGADMIN;

-- Verify current account
SELECT CURRENT_ACCOUNT_NAME();

-- Enable ORGADMIN on specific accounts
-- Replace <ACCOUNT_NAME> with the target account name
-- Run SHOW ACCOUNTS; to find available account names
ALTER ACCOUNT <ACCOUNT_NAME> SET IS_ORG_ADMIN = TRUE;
```

### Allowlist the Seemore IP <a href="#allowlist-the-datamaze-ip-0-15" id="allowlist-the-datamaze-ip-0-15"></a>

If you are using the IP allowlist in your Snowflake instance, you must add the Seemore IP to the allowlist.

(If you are not using the IP Allowlist in your Snowflake instance, you can skip this step.)

{% code lineNumbers="true" %}

```sql
CREATE NETWORK POLICY seemore_production ALLOWED_IP_LIST=('3.78.110.132', '3.73.171.134');
ALTER USER seemore_user SET NETWORK_POLICY = seemore_production;
```

{% endcode %}

### Complete Setup Script (All Permissions) <a href="#complete-setup-script" id="complete-setup-script"></a>

This is the all-in-one version of everything on this page. Run it as `ACCOUNTADMIN` to provision the role, user, warehouse, core monitoring privileges, and **all** optional capabilities — streams / dynamic-table / materialized-view listing, auto-clustering recommendations, and Cortex Code cost controls — in a single step.

Use this if you want to enable every SeeMore Data feature at once. If you only need the basic integration, use the [Core Setup Script](#comprehensive-setup-script) above instead and add optional capabilities later.

Before running, replace:

* `<public_key>` — the contents of your `rsa_key.pub` (without the `-----BEGIN/END PUBLIC KEY-----` lines). See [Create a user](#create-a-user-0-1) for how to generate the key pair.
* `seemore_warehouse` — keep it to create a dedicated warehouse, or replace it with an existing warehouse name and comment out the `CREATE WAREHOUSE` block.

{% hint style="info" %}
Optional blocks are labeled inside the script — comment out any capability you don't want to enable. Enabling `ORGADMIN` is a separate step (see [Enable ORGADMIN](#enable-orgadmin)) because it uses a different Snowflake role.
{% endhint %}

{% code lineNumbers="true" expandable="true" %}

```sql
-- ============================================================
-- SeeMore Data — Complete Snowflake Setup Script (All Permissions)
-- Run as ACCOUNTADMIN. Replace <public_key> before running.
-- ============================================================

USE ROLE ACCOUNTADMIN;

-- ------------------------------------------------------------
-- 1. Warehouse (optional)
-- Comment out this block to use an existing warehouse instead,
-- and replace "seemore_warehouse" everywhere below with its name.
-- ------------------------------------------------------------
CREATE WAREHOUSE IF NOT EXISTS seemore_warehouse
    WAREHOUSE_SIZE = 'XSMALL'
    AUTO_SUSPEND = 60
    INITIALLY_SUSPENDED = TRUE
    COMMENT = 'Used by seemore';

-- ------------------------------------------------------------
-- 2. Role
-- ------------------------------------------------------------
CREATE ROLE IF NOT EXISTS seemore_user_role;
GRANT OPERATE, USAGE ON WAREHOUSE seemore_warehouse TO ROLE seemore_user_role;

-- ------------------------------------------------------------
-- 3. User
-- ------------------------------------------------------------
CREATE USER IF NOT EXISTS seemore_user
    RSA_PUBLIC_KEY = '<public_key>'
    DEFAULT_ROLE = seemore_user_role
    DEFAULT_WAREHOUSE = 'seemore_warehouse'
    DISPLAY_NAME = 'SeemoreData';

GRANT ROLE seemore_user_role TO USER seemore_user;

-- ------------------------------------------------------------
-- 4. Core monitoring privileges
-- ------------------------------------------------------------
-- Import metadata from the Snowflake database.
GRANT IMPORTED PRIVILEGES ON DATABASE SNOWFLAKE TO ROLE seemore_user_role;

-- Monitor usage on the entire account.
GRANT MONITOR USAGE ON ACCOUNT TO ROLE seemore_user_role;

-- Monitor warehouse settings. Also required for query statistics via
-- get_query_operator_stats and for smart pulse, auto-shutdown, and auto-scaler.
GRANT MANAGE WAREHOUSES ON ACCOUNT TO ROLE seemore_user_role;

-- ------------------------------------------------------------
-- 5. Seemore database & schema (hosts the helper procedures below)
-- ------------------------------------------------------------
CREATE DATABASE IF NOT EXISTS seemore;
CREATE SCHEMA IF NOT EXISTS seemore.utils;

GRANT USAGE ON DATABASE seemore TO ROLE seemore_user_role;
GRANT USAGE ON SCHEMA seemore.utils TO ROLE seemore_user_role;

-- ------------------------------------------------------------
-- 6. Streams, Dynamic Tables & Materialized Views listing (optional)
-- EXECUTE AS OWNER so the procedure can list all objects in the account.
-- ------------------------------------------------------------
CREATE OR REPLACE PROCEDURE seemore.utils.list_object(OBJECT_KEY VARCHAR)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
    var sql_command;
    var key = OBJECT_KEY.toLowerCase();

    if (key === 'streams') {
        sql_command = 'SHOW STREAMS IN ACCOUNT';
    } else if (key === 'dynamic_tables') {
        sql_command = 'SHOW DYNAMIC TABLES IN ACCOUNT';
    } else if (key === 'materialized_views') {
        sql_command = 'SHOW MATERIALIZED VIEWS IN ACCOUNT';
    } else {
        throw 'Invalid object type. Use one of: "streams", "dynamic_tables", "materialized_views"';
    }

    var stmt = snowflake.createStatement({ sqlText: sql_command });
    var result = stmt.execute();

    var objectsList = [];
    while (result.next()) {
        var rowObj = {};
        for (var i = 1; i <= result.getColumnCount(); i++) {
            var colName = result.getColumnName(i);
            var colValue = result.getColumnValue(i);
            rowObj[colName] = colValue;
        }
        objectsList.push(rowObj);
    }

    return objectsList;
$$;

GRANT USAGE ON PROCEDURE seemore.utils.list_object(VARCHAR) TO ROLE seemore_user_role;

-- ------------------------------------------------------------
-- 7. Auto-clustering recommendations (optional)
-- Metadata only — your actual data stays private.
-- ------------------------------------------------------------
CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.GET_CLUSTERING_INFO(
  table_fqn STRING,
  column_combinations ARRAY
)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  // Validate inputs
  if (!TABLE_FQN || typeof TABLE_FQN !== 'string' || TABLE_FQN.trim() === '') {
    throw new Error('table_fqn must be a non-empty string');
  }

  if (!COLUMN_COMBINATIONS || !Array.isArray(COLUMN_COMBINATIONS) || COLUMN_COMBINATIONS.length === 0) {
    throw new Error('column_combinations must be a non-empty array');
  }

  const results = [];

  // Process each column combination
  for (let i = 0; i < COLUMN_COMBINATIONS.length; i++) {
    const combination = COLUMN_COMBINATIONS[i];

    // Validate combination is an array
    if (!Array.isArray(combination) || combination.length === 0) {
      // Skip invalid combinations but log them
      results.push({
        combination: [],
        error: 'Invalid combination: must be a non-empty array',
        metrics: null
      });
      continue;
    }

    try {
      // Escape column names for SQL (handle quotes and special characters)
      const escapedColumns = combination.map(col => {
        if (typeof col !== 'string') {
          throw new Error(`Column name must be a string, got: ${typeof col}`);
        }
        return col;
      });

      // Build column list string for SYSTEM$CLUSTERING_INFORMATION
      // Format: (col1, col2, col3)
      const columnList = '(' + escapedColumns.join(', ') + ')';

      // Build the SQL statement - use parameterized query to prevent SQL injection
      const sql = `SELECT SYSTEM$CLUSTERING_INFORMATION('${TABLE_FQN}', '${columnList}') AS clustering_info`;

      // Execute the statement
      const stmt = snowflake.createStatement({ sqlText: sql });
      const resultSet = stmt.execute();

      if (resultSet.next()) {
        const clusteringInfo = resultSet.getColumnValue(1);

        // Parse the JSON response from SYSTEM$CLUSTERING_INFORMATION
        let metrics;
        if (typeof clusteringInfo === 'string') {
          try {
            metrics = JSON.parse(clusteringInfo);
          } catch (parseErr) {
            throw new Error(`Failed to parse clustering info JSON: ${parseErr.message}`);
          }
        } else if (clusteringInfo && typeof clusteringInfo === 'object') {
          metrics = clusteringInfo;
        } else {
          throw new Error(`Unexpected clustering info format: ${typeof clusteringInfo}`);
        }

        // Build result object with all relevant metrics
        results.push({
          combination: combination,
          metrics: {
            average_depth: metrics.average_depth !== undefined ? metrics.average_depth : null,
            average_overlaps: metrics.average_overlaps !== undefined ? metrics.average_overlaps : null,
            total_partition_count: metrics.total_partition_count !== undefined ? metrics.total_partition_count : null,
            total_constant_partition_count: metrics.total_constant_partition_count !== undefined ? metrics.total_constant_partition_count : null,
            cluster_by_keys: metrics.cluster_by_keys || null,
            notes: metrics.notes || null,
            partition_depth_histogram: metrics.partition_depth_histogram || null
          }
        });
      } else {
        // No result returned
        results.push({
          combination: combination,
          error: 'No result returned from SYSTEM$CLUSTERING_INFORMATION',
          metrics: null
        });
      }
    } catch (err) {
      // Log error but continue with other combinations
      const errorMessage = err.message || String(err);
      results.push({
        combination: combination,
        error: errorMessage,
        metrics: null
      });
    }
  }

  // Return results as VARIANT (automatically converted from JavaScript array/object)
  return results;
$$;

GRANT USAGE ON PROCEDURE SEEMORE.UTILS.GET_CLUSTERING_INFO(VARCHAR, ARRAY) TO ROLE seemore_user_role;

CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.GET_CLUSTERING_COST_ESTIMATE(
  table_fqn STRING,
  column_combination ARRAY
)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  // Validate inputs
  if (!TABLE_FQN || typeof TABLE_FQN !== 'string' || TABLE_FQN.trim() === '') {
    throw new Error('table_fqn must be a non-empty string');
  }

  if (!COLUMN_COMBINATION || !Array.isArray(COLUMN_COMBINATION) || COLUMN_COMBINATION.length === 0) {
    throw new Error('column_combination must be a non-empty array');
  }

  try {
    // Escape column names for SQL (handle quotes and special characters)
    const escapedColumns = COLUMN_COMBINATION.map(col => {
      if (typeof col !== 'string') {
        throw new Error(`Column name must be a string, got: ${typeof col}`);
      }
      // Escape double quotes by doubling them
      const escaped = col.replace(/"/g, '""');
      return `"${escaped}"`;
    });

    // Build column list string for SYSTEM$ESTIMATE_AUTOMATIC_CLUSTERING_COSTS
    // Format: (col1, col2, col3)
    const columnList = '(' + escapedColumns.join(', ') + ')';

    // Build the SQL statement
    const sql = `SELECT SYSTEM$ESTIMATE_AUTOMATIC_CLUSTERING_COSTS('${TABLE_FQN}', '${columnList}') AS cost_estimation`;

    // Execute the statement
    const stmt = snowflake.createStatement({ sqlText: sql });
    const resultSet = stmt.execute();

    if (resultSet.next()) {
      const costEstimationJson = resultSet.getColumnValue(1);

      // Parse the JSON response from SYSTEM$ESTIMATE_AUTOMATIC_CLUSTERING_COSTS
      let costEstimation;
      if (typeof costEstimationJson === 'string') {
        try {
          costEstimation = JSON.parse(costEstimationJson);
        } catch (parseErr) {
          throw new Error(`Failed to parse cost estimation JSON: ${parseErr.message}`);
        }
      } else if (costEstimationJson && typeof costEstimationJson === 'object') {
        costEstimation = costEstimationJson;
      } else {
        throw new Error(`Unexpected cost estimation format: ${typeof costEstimationJson}`);
      }

      // Return result object with combination and costs
      return {
        combination: COLUMN_COMBINATION,
        costs: {
          initial: costEstimation.initial !== undefined ? costEstimation.initial : null,
          maintenance: costEstimation.maintenance !== undefined ? costEstimation.maintenance : null
        }
      };
    } else {
      // No result returned
      throw new Error('No result returned from SYSTEM$ESTIMATE_AUTOMATIC_CLUSTERING_COSTS');
    }
  } catch (err) {
    // Return error information
    const errorMessage = err.message || String(err);
    return {
      combination: COLUMN_COMBINATION,
      error: errorMessage,
      costs: null
    };
  }
$$;

GRANT USAGE ON PROCEDURE SEEMORE.UTILS.GET_CLUSTERING_COST_ESTIMATE(VARCHAR, ARRAY) TO ROLE seemore_user_role;

-- ------------------------------------------------------------
-- 8. Cortex Code cost controls (optional)
-- These procedures only modify Cortex Code credit-limit parameters —
-- they do not access any user data or query history.
-- ------------------------------------------------------------

-- 8.1 SET_CORTEX_CODE_LIMIT — set a daily estimated credit usage limit
-- at the account or user level.
CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.SET_CORTEX_CODE_LIMIT(
  P_TARGET STRING,
  P_USERNAME STRING,
  P_PARAMETER_NAME STRING,
  P_VALUE STRING
)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  var ALLOWED_PARAMS = [
    'CORTEX_CODE_CLI_DAILY_EST_CREDIT_LIMIT_PER_USER',
    'CORTEX_CODE_SNOWSIGHT_DAILY_EST_CREDIT_LIMIT_PER_USER',
    'CORTEX_CODE_DESKTOP_DAILY_EST_CREDIT_LIMIT_PER_USER'
  ];

  if (!P_PARAMETER_NAME || ALLOWED_PARAMS.indexOf(P_PARAMETER_NAME) === -1) {
    throw new Error('Invalid parameter name: ' + P_PARAMETER_NAME +
      '. Allowed: ' + ALLOWED_PARAMS.join(', '));
  }

  var numericValue = parseInt(P_VALUE, 10);
  if (isNaN(numericValue)) {
    throw new Error('P_VALUE must be a valid integer, got: ' + P_VALUE);
  }

  var target = (P_TARGET || '').toLowerCase();
  if (target !== 'account' && target !== 'user') {
    throw new Error('P_TARGET must be "account" or "user"');
  }

  var sql;
  if (target === 'account') {
    sql = 'ALTER ACCOUNT SET ' + P_PARAMETER_NAME + ' = ' + numericValue;
  } else {
    if (!P_USERNAME || typeof P_USERNAME !== 'string' || P_USERNAME.trim() === '') {
      throw new Error('P_USERNAME is required when target is "user"');
    }
    var escapedUsername = P_USERNAME.replace(/"/g, '""');
    sql = 'ALTER USER "' + escapedUsername + '" SET ' + P_PARAMETER_NAME + ' = ' + numericValue;
  }

  var stmt = snowflake.createStatement({ sqlText: sql });
  stmt.execute();

  return { status: 'success', target: target, parameter: P_PARAMETER_NAME, value: numericValue };
$$;

GRANT USAGE ON PROCEDURE SEEMORE.UTILS.SET_CORTEX_CODE_LIMIT(STRING, STRING, STRING, STRING) TO ROLE seemore_user_role;

-- 8.2 UNSET_CORTEX_CODE_LIMIT — remove a previously set credit limit,
-- restoring the default (unlimited).
CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.UNSET_CORTEX_CODE_LIMIT(
  P_TARGET STRING,
  P_USERNAME STRING,
  P_PARAMETER_NAME STRING
)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  var ALLOWED_PARAMS = [
    'CORTEX_CODE_CLI_DAILY_EST_CREDIT_LIMIT_PER_USER',
    'CORTEX_CODE_SNOWSIGHT_DAILY_EST_CREDIT_LIMIT_PER_USER',
    'CORTEX_CODE_DESKTOP_DAILY_EST_CREDIT_LIMIT_PER_USER'
  ];

  if (!P_PARAMETER_NAME || ALLOWED_PARAMS.indexOf(P_PARAMETER_NAME) === -1) {
    throw new Error('Invalid parameter name: ' + P_PARAMETER_NAME +
      '. Allowed: ' + ALLOWED_PARAMS.join(', '));
  }

  var target = (P_TARGET || '').toLowerCase();
  if (target !== 'account' && target !== 'user') {
    throw new Error('P_TARGET must be "account" or "user"');
  }

  var sql;
  if (target === 'account') {
    sql = 'ALTER ACCOUNT UNSET ' + P_PARAMETER_NAME;
  } else {
    if (!P_USERNAME || typeof P_USERNAME !== 'string' || P_USERNAME.trim() === '') {
      throw new Error('P_USERNAME is required when target is "user"');
    }
    var escapedUsername = P_USERNAME.replace(/"/g, '""');
    sql = 'ALTER USER "' + escapedUsername + '" UNSET ' + P_PARAMETER_NAME;
  }

  var stmt = snowflake.createStatement({ sqlText: sql });
  stmt.execute();

  return { status: 'success', target: target, parameter: P_PARAMETER_NAME };
$$;

GRANT USAGE ON PROCEDURE SEEMORE.UTILS.UNSET_CORTEX_CODE_LIMIT(STRING, STRING, STRING) TO ROLE seemore_user_role;

-- 8.3 GET_CORTEX_CODE_ACCOUNT_LIMITS — read current account-level limits.
CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.GET_CORTEX_CODE_ACCOUNT_LIMITS()
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  var stmt = snowflake.createStatement({ sqlText: "SHOW PARAMETERS LIKE 'CORTEX_CODE_%' IN ACCOUNT" });
  var result = stmt.execute();

  var rows = [];
  while (result.next()) {
    var row = {};
    for (var i = 1; i <= result.getColumnCount(); i++) {
      row[result.getColumnName(i)] = result.getColumnValue(i);
    }
    rows.push(row);
  }

  // Version marker: lets Seemore detect that these utilities support the Desktop
  // channel. Bump this whenever the set of managed parameters changes.
  rows.push({ key: 'SEEMORE_CORTEX_CODE_UTILS_VERSION', value: '2', level: 'ACCOUNT', type: 'NUMBER' });

  return rows;
$$;

GRANT USAGE ON PROCEDURE SEEMORE.UTILS.GET_CORTEX_CODE_ACCOUNT_LIMITS() TO ROLE seemore_user_role;

-- 8.4 GET_CORTEX_CODE_USER_LIMITS — read current limits for a specific user.
CREATE OR REPLACE PROCEDURE SEEMORE.UTILS.GET_CORTEX_CODE_USER_LIMITS(
  P_USERNAME STRING
)
RETURNS VARIANT
LANGUAGE JAVASCRIPT
EXECUTE AS OWNER
AS
$$
  if (!P_USERNAME || typeof P_USERNAME !== 'string' || P_USERNAME.trim() === '') {
    throw new Error('P_USERNAME must be a non-empty string');
  }

  var escapedUsername = P_USERNAME.replace(/"/g, '""');
  var sql = "SHOW PARAMETERS LIKE 'CORTEX_CODE_%' IN USER \"" + escapedUsername + "\"";

  var stmt = snowflake.createStatement({ sqlText: sql });
  var result = stmt.execute();

  var rows = [];
  while (result.next()) {
    var row = {};
    for (var i = 1; i <= result.getColumnCount(); i++) {
      row[result.getColumnName(i)] = result.getColumnValue(i);
    }
    rows.push(row);
  }

  return rows;
$$;

GRANT USAGE ON PROCEDURE SEEMORE.UTILS.GET_CORTEX_CODE_USER_LIMITS(STRING) TO ROLE seemore_user_role;

-- ------------------------------------------------------------
-- 9. IP allowlist (optional)
-- Only needed if you use a Snowflake network policy / IP allowlist.
-- Uncomment to allow SeeMore Data's IPs and apply the policy to the user.
-- ------------------------------------------------------------
-- CREATE NETWORK POLICY seemore_production ALLOWED_IP_LIST = ('3.78.110.132', '3.73.171.134');
-- ALTER USER seemore_user SET NETWORK_POLICY = seemore_production;
```

{% endcode %}
