Skip to content

Commit

Permalink
fix: Small fixes (#3337)
Browse files Browse the repository at this point in the history
Fixes:
- add missing resource monitor as valid option to `snowflake_grant_ownership` (references: [#3318](#3318))
- clarify docs for `snowflake_views` (references: [#3203](#3203))
- add timeouts to `snowflake_execute` (references: [#3334](#3334))

Misc (mostly regarding acceptance tests):
- remove vacations notice from readme
- document current behavior of multiline attributes in cofig generators when there is whitespace (like `\t`); result of #3307 (comment)
- extract okta url to test data
  • Loading branch information
sfc-gh-asawicki authored and sfc-gh-jcieslak committed Jan 20, 2025
1 parent 4eae4c8 commit 8807839
Show file tree
Hide file tree
Showing 21 changed files with 297 additions and 53 deletions.
10 changes: 10 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ across different versions.
### Fixed migration of account resource
Previously, during upgrading the provider from v0.99.0, when account fields `must_change_password` or `is_org_admin` were not set in state, the provider panicked. It has been fixed in this version.

### Add missing resource monitor in `snowflake_grant_ownership` resource
Resource monitor in not currently listed as option in `GRANT OWNERSHIP` documentation ([here](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership#required-parameters)) but this is a valid option. `snowflake_grant_ownership` was updated to support resource monitors.

References: [#3318](/~https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/3318)

### Timeouts in `snowflake_execute`
By default, resource operation timeouts after 20 minutes ([reference](https://developer.hashicorp.com/terraform/plugin/sdkv2/resources/retries-and-customizable-timeouts#default-timeouts-and-deadline-exceeded-errors)). Because of generic nature of `snowflake_execute`, we decided to bump its default timeouts to 60 minutes; We also allowed setting them on the resource config level (following [official documentation](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts)).

References: [#3334](/~https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/3334)

## v1.0.0 ➞ v1.0.1

### Fixes in account parameters
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Snowflake Terraform Provider

> ⚠️ **Incoming holidays**: The whole team will be absent from the 21st of December to the 7th of January. Our involvement in GitHub issues during this time will be limited. We will look out for the critical issues, though. Merry Christmas and a Happy New Year!
> ⚠️ **Please note**: If you believe you have found a security issue, _please responsibly disclose_ by contacting us at [triage-terraformprovider-dl@snowflake.com](mailto:triage-terraformprovider-dl@snowflake.com).
> ⚠️ **Disclaimer**: The project is in v1 version, but some features are in preview. Such resources and data sources are considered preview features in the provider, regardless of their state in Snowflake. We do not guarantee their stability. They will be reworked and marked as a stable feature in future releases. Breaking changes in these features are expected, even without bumping the major version. They are disabled by default. To use them, add the relevant feature name to `preview_features_enabled` field in the [provider configuration](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs#schema). The list of preview features is available below. Please always refer to the [Getting Help](/~https://github.com/Snowflake-Labs/terraform-provider-snowflake?tab=readme-ov-file#getting-help) section in our Github repo to best determine how to get help for your questions.
Expand Down
16 changes: 12 additions & 4 deletions docs/data-sources/views.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,22 @@ output "limit_output" {
}
# Filtering (in)
data "snowflake_views" "in" {
data "snowflake_views" "in_account" {
in {
database = "database"
account = true
}
}
output "in_output" {
value = data.snowflake_views.in.views
data "snowflake_views" "in_database" {
in {
database = "<database_name>"
}
}
data "snowflake_views" "in_schema" {
in {
schema = "<database_name>.<schema_name>"
}
}
# Without additional data (to limit the number of calls make for every found view)
Expand Down
11 changes: 11 additions & 0 deletions docs/resources/execute.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,23 @@ resource "snowflake_execute" "test" {
### Optional

- `query` (String) Optional SQL statement to do a read. Invoked on every resource refresh and every time it is changed.
- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))

### Read-Only

- `id` (String) The ID of this resource.
- `query_results` (List of Map of String) List of key-value maps (text to text) retrieved after executing read query. Will be empty if the query results in an error.

<a id="nestedblock--timeouts"></a>
### Nested Schema for `timeouts`

Optional:

- `create` (String)
- `delete` (String)
- `read` (String)
- `update` (String)

## Import

Import is supported using the following syntax:
Expand Down
6 changes: 3 additions & 3 deletions docs/resources/grant_ownership.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,14 +266,14 @@ Optional:
- `all` (Block List, Max: 1) Configures the privilege to be granted on all objects in either a database or schema. (see [below for nested schema](#nestedblock--on--all))
- `future` (Block List, Max: 1) Configures the privilege to be granted on all objects in either a database or schema. (see [below for nested schema](#nestedblock--on--future))
- `object_name` (String) Specifies the identifier for the object on which you are transferring ownership.
- `object_type` (String) Specifies the type of object on which you are transferring ownership. Available values are: AGGREGATION POLICY | ALERT | AUTHENTICATION POLICY | COMPUTE POOL | DATA METRIC FUNCTION | DATABASE | DATABASE ROLE | DYNAMIC TABLE | EVENT TABLE | EXTERNAL TABLE | EXTERNAL VOLUME | FAILOVER GROUP | FILE FORMAT | FUNCTION | GIT REPOSITORY | HYBRID TABLE | ICEBERG TABLE | IMAGE REPOSITORY | INTEGRATION | MATERIALIZED VIEW | NETWORK POLICY | NETWORK RULE | PACKAGES POLICY | PIPE | PROCEDURE | MASKING POLICY | PASSWORD POLICY | PROJECTION POLICY | REPLICATION GROUP | ROLE | ROW ACCESS POLICY | SCHEMA | SESSION POLICY | SECRET | SEQUENCE | STAGE | STREAM | TABLE | TAG | TASK | USER | VIEW | WAREHOUSE
- `object_type` (String) Specifies the type of object on which you are transferring ownership. Available values are: AGGREGATION POLICY | ALERT | AUTHENTICATION POLICY | COMPUTE POOL | DATA METRIC FUNCTION | DATABASE | DATABASE ROLE | DYNAMIC TABLE | EVENT TABLE | EXTERNAL TABLE | EXTERNAL VOLUME | FAILOVER GROUP | FILE FORMAT | FUNCTION | GIT REPOSITORY | HYBRID TABLE | ICEBERG TABLE | IMAGE REPOSITORY | INTEGRATION | MATERIALIZED VIEW | NETWORK POLICY | NETWORK RULE | PACKAGES POLICY | PIPE | PROCEDURE | MASKING POLICY | PASSWORD POLICY | PROJECTION POLICY | REPLICATION GROUP | RESOURCE MONITOR | ROLE | ROW ACCESS POLICY | SCHEMA | SESSION POLICY | SECRET | SEQUENCE | STAGE | STREAM | TABLE | TAG | TASK | USER | VIEW | WAREHOUSE

<a id="nestedblock--on--all"></a>
### Nested Schema for `on.all`

Required:

- `object_type_plural` (String) Specifies the type of object in plural form on which you are transferring ownership. Available values are: AGGREGATION POLICIES | ALERTS | AUTHENTICATION POLICIES | COMPUTE POOLS | DATA METRIC FUNCTIONS | DATABASES | DATABASE ROLES | DYNAMIC TABLES | EVENT TABLES | EXTERNAL TABLES | EXTERNAL VOLUMES | FAILOVER GROUPS | FILE FORMATS | FUNCTIONS | GIT REPOSITORIES | HYBRID TABLES | ICEBERG TABLES | IMAGE REPOSITORIES | INTEGRATIONS | MATERIALIZED VIEWS | NETWORK POLICIES | NETWORK RULES | PACKAGES POLICIES | PIPES | PROCEDURES | MASKING POLICIES | PASSWORD POLICIES | PROJECTION POLICIES | REPLICATION GROUPS | ROLES | ROW ACCESS POLICIES | SCHEMAS | SESSION POLICIES | SECRETS | SEQUENCES | STAGES | STREAMS | TABLES | TAGS | TASKS | USERS | VIEWS | WAREHOUSES. For more information head over to [Snowflake documentation](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership#required-parameters).
- `object_type_plural` (String) Specifies the type of object in plural form on which you are transferring ownership. Available values are: AGGREGATION POLICIES | ALERTS | AUTHENTICATION POLICIES | COMPUTE POOLS | DATA METRIC FUNCTIONS | DATABASES | DATABASE ROLES | DYNAMIC TABLES | EVENT TABLES | EXTERNAL TABLES | EXTERNAL VOLUMES | FAILOVER GROUPS | FILE FORMATS | FUNCTIONS | GIT REPOSITORIES | HYBRID TABLES | ICEBERG TABLES | IMAGE REPOSITORIES | INTEGRATIONS | MATERIALIZED VIEWS | NETWORK POLICIES | NETWORK RULES | PACKAGES POLICIES | PIPES | PROCEDURES | MASKING POLICIES | PASSWORD POLICIES | PROJECTION POLICIES | REPLICATION GROUPS | RESOURCE MONITORS | ROLES | ROW ACCESS POLICIES | SCHEMAS | SESSION POLICIES | SECRETS | SEQUENCES | STAGES | STREAMS | TABLES | TAGS | TASKS | USERS | VIEWS | WAREHOUSES. For more information head over to [Snowflake documentation](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership#required-parameters).

Optional:

Expand All @@ -286,7 +286,7 @@ Optional:

Required:

- `object_type_plural` (String) Specifies the type of object in plural form on which you are transferring ownership. Available values are: AGGREGATION POLICIES | ALERTS | AUTHENTICATION POLICIES | COMPUTE POOLS | DATA METRIC FUNCTIONS | DATABASES | DATABASE ROLES | DYNAMIC TABLES | EVENT TABLES | EXTERNAL TABLES | EXTERNAL VOLUMES | FAILOVER GROUPS | FILE FORMATS | FUNCTIONS | GIT REPOSITORIES | HYBRID TABLES | ICEBERG TABLES | IMAGE REPOSITORIES | INTEGRATIONS | MATERIALIZED VIEWS | NETWORK POLICIES | NETWORK RULES | PACKAGES POLICIES | PIPES | PROCEDURES | MASKING POLICIES | PASSWORD POLICIES | PROJECTION POLICIES | REPLICATION GROUPS | ROLES | ROW ACCESS POLICIES | SCHEMAS | SESSION POLICIES | SECRETS | SEQUENCES | STAGES | STREAMS | TABLES | TAGS | TASKS | USERS | VIEWS | WAREHOUSES. For more information head over to [Snowflake documentation](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership#required-parameters).
- `object_type_plural` (String) Specifies the type of object in plural form on which you are transferring ownership. Available values are: AGGREGATION POLICIES | ALERTS | AUTHENTICATION POLICIES | COMPUTE POOLS | DATA METRIC FUNCTIONS | DATABASES | DATABASE ROLES | DYNAMIC TABLES | EVENT TABLES | EXTERNAL TABLES | EXTERNAL VOLUMES | FAILOVER GROUPS | FILE FORMATS | FUNCTIONS | GIT REPOSITORIES | HYBRID TABLES | ICEBERG TABLES | IMAGE REPOSITORIES | INTEGRATIONS | MATERIALIZED VIEWS | NETWORK POLICIES | NETWORK RULES | PACKAGES POLICIES | PIPES | PROCEDURES | MASKING POLICIES | PASSWORD POLICIES | PROJECTION POLICIES | REPLICATION GROUPS | RESOURCE MONITORS | ROLES | ROW ACCESS POLICIES | SCHEMAS | SESSION POLICIES | SECRETS | SEQUENCES | STAGES | STREAMS | TABLES | TAGS | TASKS | USERS | VIEWS | WAREHOUSES. For more information head over to [Snowflake documentation](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership#required-parameters).

Optional:

Expand Down
16 changes: 12 additions & 4 deletions examples/data-sources/snowflake_views/data-source.tf
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,22 @@ output "limit_output" {
}

# Filtering (in)
data "snowflake_views" "in" {
data "snowflake_views" "in_account" {
in {
database = "database"
account = true
}
}

output "in_output" {
value = data.snowflake_views.in.views
data "snowflake_views" "in_database" {
in {
database = "<database_name>"
}
}

data "snowflake_views" "in_schema" {
in {
schema = "<database_name>.<schema_name>"
}
}

# Without additional data (to limit the number of calls make for every found view)
Expand Down
19 changes: 19 additions & 0 deletions pkg/acceptance/bettertestspoc/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ resource "snowflake_share" "test" {
require.Equal(t, expectedOutput, result)
})

// TODO [SNOW-1501905]: replace \t characters with actual tabs
t.Run("test tabs in multiline", func(t *testing.T) {
someModel := Some("test", "Some Name").
WithMultilineField("some\n\tmulti\tline\n\t\t\tcontent")
expectedOutput := strings.TrimPrefix(`
resource "snowflake_share" "test" {
name = "Some Name"
multiline_field = <<EOT
some
\tmulti\tline
\t\t\tcontent
EOT
}
`, "\n")
result := config.ResourceFromModel(t, someModel)

require.Equal(t, expectedOutput, result)
})

t.Run("test full", func(t *testing.T) {
someModel := Some("test", "Some Name").
WithComment("Some Comment").
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testvars"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
)

Expand Down Expand Up @@ -70,7 +71,7 @@ func (m *SnowflakeModel) AllFields(tmpConfig *helpers.TmpTomlConfig, tmpUser *he
WithValidateDefaultParameters("true").
WithClientIp("3.3.3.3").
WithAuthenticatorType(sdk.AuthenticationTypeJwt).
WithOktaUrl("https://example-tf.com").
WithOktaUrl(testvars.ExampleOktaUrlString).
WithLoginTimeout(101).
WithRequestTimeout(201).
WithJwtExpireTimeout(301).
Expand Down
5 changes: 3 additions & 2 deletions pkg/acceptance/helpers/config_toml_creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers/random"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testvars"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
)

Expand All @@ -25,7 +26,7 @@ warehouse = '%[4]s'
clientip = '1.2.3.4'
protocol = 'https'
port = 443
oktaurl = 'https://example.com'
oktaurl = '%[8]s'
clienttimeout = 10
jwtclienttimeout = 20
logintimeout = 30
Expand All @@ -50,7 +51,7 @@ disableconsolelogin = true
[%[1]s.params]
foo = 'bar'
`, profile, userId.Name(), roleId.Name(), warehouseId.Name(), accountIdentifier.OrganizationName(), accountIdentifier.AccountName(), privateKey)
`, profile, userId.Name(), roleId.Name(), warehouseId.Name(), accountIdentifier.OrganizationName(), accountIdentifier.AccountName(), privateKey, testvars.ExampleOktaUrlString)
}

// FullInvalidTomlConfigForServiceUser is a temporary function used to test provider configuration
Expand Down
13 changes: 13 additions & 0 deletions pkg/acceptance/testvars/okta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package testvars

import "net/url"

var (
ExampleOktaUrlString = "https://example-tf.com"
ExampleOktaUrl, _ = url.Parse(ExampleOktaUrlString)
)

var (
ExampleOktaUrlFromEnvString = "https://example-tf-env.com"
ExampleOktaUrlFromEnv, _ = url.Parse(ExampleOktaUrlFromEnvString)
)
21 changes: 6 additions & 15 deletions pkg/provider/provider_acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package provider_test
import (
"fmt"
"net"
"net/url"
"os"
"regexp"
"strings"
Expand All @@ -21,6 +20,7 @@ import (
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/ids"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testprofiles"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testvars"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/snowflakeenvs"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/previewfeatures"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
Expand Down Expand Up @@ -234,9 +234,6 @@ func TestAcc_Provider_tomlConfig(t *testing.T) {
return helpers.FullTomlConfigForServiceUser(t, profile, tmpServiceUser.UserId, tmpServiceUser.RoleId, tmpServiceUser.WarehouseId, tmpServiceUser.AccountId, tmpServiceUser.PrivateKey)
})

oktaUrl, err := url.Parse("https://example.com")
require.NoError(t, err)

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() {
Expand Down Expand Up @@ -266,7 +263,7 @@ func TestAcc_Provider_tomlConfig(t *testing.T) {
assert.Equal(t, 443, config.Port)
assert.Equal(t, gosnowflake.AuthTypeJwt, config.Authenticator)
assert.Equal(t, false, config.PasscodeInPassword)
assert.Equal(t, oktaUrl, config.OktaURL)
assert.Equal(t, testvars.ExampleOktaUrl, config.OktaURL)
assert.Equal(t, 30*time.Second, config.LoginTimeout)
assert.Equal(t, 40*time.Second, config.RequestTimeout)
assert.Equal(t, 50*time.Second, config.JWTExpireTimeout)
Expand Down Expand Up @@ -309,9 +306,6 @@ func TestAcc_Provider_envConfig(t *testing.T) {
return helpers.FullInvalidTomlConfigForServiceUser(t, profile)
})

oktaUrlFromEnv, err := url.Parse("https://example-env.com")
require.NoError(t, err)

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() {
Expand Down Expand Up @@ -342,7 +336,7 @@ func TestAcc_Provider_envConfig(t *testing.T) {
t.Setenv(snowflakeenvs.Host, "")
t.Setenv(snowflakeenvs.Passcode, "")
t.Setenv(snowflakeenvs.PasscodeInPassword, "false")
t.Setenv(snowflakeenvs.OktaUrl, "https://example-env.com")
t.Setenv(snowflakeenvs.OktaUrl, testvars.ExampleOktaUrlFromEnvString)
t.Setenv(snowflakeenvs.LoginTimeout, "100")
t.Setenv(snowflakeenvs.RequestTimeout, "200")
t.Setenv(snowflakeenvs.JwtExpireTimeout, "300")
Expand Down Expand Up @@ -377,7 +371,7 @@ func TestAcc_Provider_envConfig(t *testing.T) {
assert.Equal(t, 443, config.Port)
assert.Equal(t, gosnowflake.AuthTypeJwt, config.Authenticator)
assert.Equal(t, false, config.PasscodeInPassword)
assert.Equal(t, oktaUrlFromEnv, config.OktaURL)
assert.Equal(t, testvars.ExampleOktaUrlFromEnv, config.OktaURL)
assert.Equal(t, 100*time.Second, config.LoginTimeout)
assert.Equal(t, 200*time.Second, config.RequestTimeout)
assert.Equal(t, 300*time.Second, config.JWTExpireTimeout)
Expand Down Expand Up @@ -420,9 +414,6 @@ func TestAcc_Provider_tfConfig(t *testing.T) {
return helpers.FullInvalidTomlConfigForServiceUser(t, profile)
})

oktaUrlFromTf, err := url.Parse("https://example-tf.com")
require.NoError(t, err)

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() {
Expand Down Expand Up @@ -454,7 +445,7 @@ func TestAcc_Provider_tfConfig(t *testing.T) {
t.Setenv(snowflakeenvs.Authenticator, "invalid")
t.Setenv(snowflakeenvs.Passcode, "")
t.Setenv(snowflakeenvs.PasscodeInPassword, "false")
t.Setenv(snowflakeenvs.OktaUrl, "https://example-env.com")
t.Setenv(snowflakeenvs.OktaUrl, testvars.ExampleOktaUrlFromEnvString)
t.Setenv(snowflakeenvs.LoginTimeout, "100")
t.Setenv(snowflakeenvs.RequestTimeout, "200")
t.Setenv(snowflakeenvs.JwtExpireTimeout, "300")
Expand Down Expand Up @@ -489,7 +480,7 @@ func TestAcc_Provider_tfConfig(t *testing.T) {
assert.Equal(t, 443, config.Port)
assert.Equal(t, gosnowflake.AuthTypeJwt, config.Authenticator)
assert.Equal(t, false, config.PasscodeInPassword)
assert.Equal(t, oktaUrlFromTf, config.OktaURL)
assert.Equal(t, testvars.ExampleOktaUrl, config.OktaURL)
assert.Equal(t, 101*time.Second, config.LoginTimeout)
assert.Equal(t, 201*time.Second, config.RequestTimeout)
assert.Equal(t, 301*time.Second, config.JWTExpireTimeout)
Expand Down
17 changes: 10 additions & 7 deletions pkg/resources/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import (
"context"
"fmt"
"log"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources"
"time"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

Expand Down Expand Up @@ -58,6 +55,12 @@ func Execute() *schema.Resource {
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(60 * time.Minute),
Read: schema.DefaultTimeout(60 * time.Minute),
Update: schema.DefaultTimeout(60 * time.Minute),
Delete: schema.DefaultTimeout(60 * time.Minute),
},

Description: "Resource allowing execution of ANY SQL statement.",

Expand Down
Loading

0 comments on commit 8807839

Please sign in to comment.