Skip to content

Commit

Permalink
feat: Add views to the SDK (#2171)
Browse files Browse the repository at this point in the history
* Prepare view definition (WIP)

* Add column list and column masking policies (WIP)

* Finish create definition (WIP)

* Add test for multiple session params in task

* Add alter action to views definition

* Add describe operation to views definition

* Define view and view row

* Add generated files (without changes)

* Make compile after generation

* Pass create view unit tests (WIP)

* Test full view create

* Pass show, describe and drop tests; add alter branches tests (WIP)

* Pass alter tests for view

* Pass first integration test

* Add more complicated create integration test

* Add drop tests

* Add show tests

* Add describe test

* Add second describe test

* Add rename test

* Add alter tests (WIP)

* Add set and unset masking policy on column

* Add set and unset tag on column

* Add adding and dropping row access policies

* Add row access policy to creation

* Add comment

* Change On to required (WIP)

* Fix tests (WIP)

* Add validation tests

* Fix linter complaints

* Adjust struct definitions to current types

* Add convenience methods to db and plain structs

* Rename list method and fix tags

* Regenerate Options

* Introduce optional variants for set and unset tags

* Refactor slightly set and unset tags

* Regenerate

* Revert list to keyword

* Add jira issue to TODO comment
  • Loading branch information
sfc-gh-asawicki authored Nov 13, 2023
1 parent 6f026f6 commit ed079d3
Show file tree
Hide file tree
Showing 21 changed files with 2,322 additions and 15 deletions.
2 changes: 2 additions & 0 deletions pkg/sdk/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type Client struct {
Tags Tags
Tasks Tasks
Users Users
Views Views
Warehouses Warehouses
}

Expand Down Expand Up @@ -170,6 +171,7 @@ func (c *Client) initialize() {
c.Tags = &tags{client: c}
c.Tasks = &tasks{client: c}
c.Users = &users{client: c}
c.Views = &views{client: c}
c.Warehouses = &warehouses{client: c}
}

Expand Down
7 changes: 7 additions & 0 deletions pkg/sdk/integration_test_imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ func (c *Client) ExecForTests(ctx context.Context, sql string) (sql.Result, erro
return result, decodeDriverError(err)
}

// QueryOneForTests is an exact copy of queryOne (that is unexported), that some integration tests/helpers were using
// TODO: remove after introducing all resources using this
func (c *Client) QueryOneForTests(ctx context.Context, dest interface{}, sql string) error {
ctx = context.WithValue(ctx, snowflakeAccountLocatorContextKey, c.accountLocator)
return decodeDriverError(c.db.GetContext(ctx, dest, sql))
}

func ErrorsEqual(t *testing.T, expected error, actual error) {
t.Helper()
var expectedErr *Error
Expand Down
1 change: 1 addition & 0 deletions pkg/sdk/object_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const (
ObjectTypeApplicationPackage ObjectType = "APPLICATION PACKAGE"
ObjectTypeApplicationRole ObjectType = "APPLICATION ROLE"
ObjectTypeStreamlit ObjectType = "STREAMLIT"
ObjectTypeColumn ObjectType = "COLUMN"
)

func (o ObjectType) String() string {
Expand Down
16 changes: 16 additions & 0 deletions pkg/sdk/poc/generator/db_struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ func (v *dbStruct) Field(dbName string, kind string) *dbStruct {
return v
}

func (v *dbStruct) Text(dbName string) *dbStruct {
return v.Field(dbName, "string")
}

func (v *dbStruct) OptionalText(dbName string) *dbStruct {
return v.Field(dbName, "sql.NullString")
}

func (v *dbStruct) Bool(dbName string) *dbStruct {
return v.Field(dbName, "bool")
}

func (v *dbStruct) OptionalBool(dbName string) *dbStruct {
return v.Field(dbName, "sql.NullBool")
}

func (v *dbStruct) IntoField() *Field {
f := NewField(v.name, v.name, nil, nil)
for _, field := range v.fields {
Expand Down
13 changes: 10 additions & 3 deletions pkg/sdk/poc/generator/field_transformers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ type FieldTransformer interface {
}

type KeywordTransformer struct {
required bool
sqlPrefix string
quotes string
required bool
sqlPrefix string
quotes string
parentheses string
}

func KeywordOptions() *KeywordTransformer {
Expand Down Expand Up @@ -41,13 +42,19 @@ func (v *KeywordTransformer) DoubleQuotes() *KeywordTransformer {
return v
}

func (v *KeywordTransformer) Parentheses() *KeywordTransformer {
v.parentheses = "parentheses"
return v
}

func (v *KeywordTransformer) Transform(f *Field) *Field {
addTagIfMissing(f.Tags, "ddl", "keyword")
if v.required {
f.Required = true
}
addTagIfMissing(f.Tags, "sql", v.sqlPrefix)
addTagIfMissing(f.Tags, "ddl", v.quotes)
addTagIfMissing(f.Tags, "ddl", v.parentheses)
return f
}

Expand Down
33 changes: 29 additions & 4 deletions pkg/sdk/poc/generator/keyword_builders.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,43 @@ func (v *QueryStruct) OptionalSessionParametersUnset() *QueryStruct {
return v
}

func (v *QueryStruct) WithTags() *QueryStruct {
v.fields = append(v.fields, NewField("Tag", "[]TagAssociation", Tags().Keyword().Parentheses().SQL("TAG"), nil))
func (v *QueryStruct) NamedListWithParens(sqlPrefix string, listItemKind string, transformer *KeywordTransformer) *QueryStruct {
if transformer != nil {
transformer = transformer.Parentheses().SQL(sqlPrefix)
} else {
transformer = KeywordOptions().Parentheses().SQL(sqlPrefix)
}
v.fields = append(v.fields, NewField(sqlToFieldName(sqlPrefix, true), KindOfSlice(listItemKind), Tags().Keyword(), transformer))
return v
}

func (v *QueryStruct) WithTags() *QueryStruct {
return v.NamedListWithParens("TAG", "TagAssociation", nil)
}

func (v *QueryStruct) SetTags() *QueryStruct {
v.fields = append(v.fields, NewField("SetTags", "[]TagAssociation", Tags().Keyword().SQL("SET TAG"), nil))
return v.setTags(KeywordOptions().Required())
}

func (v *QueryStruct) OptionalSetTags() *QueryStruct {
return v.setTags(nil)
}

func (v *QueryStruct) setTags(transformer *KeywordTransformer) *QueryStruct {
v.fields = append(v.fields, NewField("SetTags", "[]TagAssociation", Tags().Keyword().SQL("SET TAG"), transformer))
return v
}

func (v *QueryStruct) UnsetTags() *QueryStruct {
v.fields = append(v.fields, NewField("UnsetTags", "[]ObjectIdentifier", Tags().Keyword().SQL("UNSET TAG"), nil))
return v.unsetTags(KeywordOptions().Required())
}

func (v *QueryStruct) OptionalUnsetTags() *QueryStruct {
return v.unsetTags(nil)
}

func (v *QueryStruct) unsetTags(transformer *KeywordTransformer) *QueryStruct {
v.fields = append(v.fields, NewField("UnsetTags", "[]ObjectIdentifier", Tags().Keyword().SQL("UNSET TAG"), transformer))
return v
}

Expand Down
16 changes: 16 additions & 0 deletions pkg/sdk/poc/generator/plain_struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ func (v *plainStruct) Field(name string, kind string) *plainStruct {
return v
}

func (v *plainStruct) Text(name string) *plainStruct {
return v.Field(name, "string")
}

func (v *plainStruct) OptionalText(name string) *plainStruct {
return v.Field(name, "*string")
}

func (v *plainStruct) Bool(name string) *plainStruct {
return v.Field(name, "bool")
}

func (v *plainStruct) OptionalBool(name string) *plainStruct {
return v.Field(name, "*bool")
}

func (v *plainStruct) IntoField() *Field {
f := NewField(v.name, v.name, nil, nil)
for _, field := range v.fields {
Expand Down
1 change: 1 addition & 0 deletions pkg/sdk/poc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var definitionMapping = map[string]*generator.Interface{
"tasks_def.go": sdk.TasksDef,
"streams_def.go": sdk.StreamsDef,
"application_roles_def.go": sdk.ApplicationRolesDef,
"views_def.go": sdk.ViewsDef,
}

func main() {
Expand Down
4 changes: 2 additions & 2 deletions pkg/sdk/session_policies_def.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ var SessionPoliciesDef = g.NewInterface(
WithValidation(g.AtLeastOneValueSet, "SessionIdleTimeoutMins", "SessionUiIdleTimeoutMins", "Comment"),
g.KeywordOptions().SQL("SET"),
).
SetTags().
UnsetTags().
OptionalSetTags().
OptionalUnsetTags().
OptionalQueryStructField(
"Unset",
g.NewQueryStruct("SessionPolicyUnset").
Expand Down
4 changes: 2 additions & 2 deletions pkg/sdk/streams_def.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ var (
Name().
OptionalTextAssignment("SET COMMENT", g.ParameterOptions().SingleQuotes()).
OptionalSQL("UNSET COMMENT").
SetTags().
UnsetTags().
OptionalSetTags().
OptionalUnsetTags().
WithValidation(g.ValidIdentifier, "name").
WithValidation(g.ConflictingFields, "IfExists", "UnsetTags").
WithValidation(g.ExactlyOneValueSet, "SetComment", "UnsetComment", "SetTags", "UnsetTags"),
Expand Down
4 changes: 2 additions & 2 deletions pkg/sdk/tasks_def.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ var TasksDef = g.NewInterface(
WithValidation(g.AtLeastOneValueSet, "Warehouse", "Schedule", "Config", "AllowOverlappingExecution", "UserTaskTimeoutMs", "SuspendTaskAfterNumFailures", "ErrorIntegration", "Comment", "SessionParametersUnset"),
g.KeywordOptions().SQL("UNSET"),
).
SetTags().
UnsetTags().
OptionalSetTags().
OptionalUnsetTags().
OptionalTextAssignment("MODIFY AS", g.ParameterOptions().NoQuotes().NoEquals()).
OptionalTextAssignment("MODIFY WHEN", g.ParameterOptions().NoQuotes().NoEquals()).
WithValidation(g.ValidIdentifier, "name").
Expand Down
5 changes: 3 additions & 2 deletions pkg/sdk/tasks_gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ func TestTasks_Create(t *testing.T) {
WithConfig(String(`$${"output_dir": "/temp/test_directory/", "learning_rate": 0.1}$$`)).
WithAllowOverlappingExecution(Bool(true)).
WithSessionParameters(&SessionParameters{
JSONIndent: Int(10),
JSONIndent: Int(10),
LockTimeout: Int(5),
}).
WithUserTaskTimeoutMs(Int(5)).
WithSuspendTaskAfterNumFailures(Int(6)).
Expand All @@ -85,7 +86,7 @@ func TestTasks_Create(t *testing.T) {
}}).
WithWhen(String(`SYSTEM$STREAM_HAS_DATA('MYSTREAM')`))

assertOptsValidAndSQLEquals(t, req.toOpts(), "CREATE OR REPLACE TASK %s WAREHOUSE = %s SCHEDULE = '10 MINUTE' CONFIG = $${\"output_dir\": \"/temp/test_directory/\", \"learning_rate\": 0.1}$$ ALLOW_OVERLAPPING_EXECUTION = true JSON_INDENT = 10 USER_TASK_TIMEOUT_MS = 5 SUSPEND_TASK_AFTER_NUM_FAILURES = 6 ERROR_INTEGRATION = some_error_integration COPY GRANTS COMMENT = 'some comment' AFTER %s TAG (%s = 'v1') WHEN SYSTEM$STREAM_HAS_DATA('MYSTREAM') AS SELECT CURRENT_TIMESTAMP", id.FullyQualifiedName(), warehouseId.FullyQualifiedName(), otherTaskId.FullyQualifiedName(), tagId.FullyQualifiedName())
assertOptsValidAndSQLEquals(t, req.toOpts(), "CREATE OR REPLACE TASK %s WAREHOUSE = %s SCHEDULE = '10 MINUTE' CONFIG = $${\"output_dir\": \"/temp/test_directory/\", \"learning_rate\": 0.1}$$ ALLOW_OVERLAPPING_EXECUTION = true JSON_INDENT = 10, LOCK_TIMEOUT = 5 USER_TASK_TIMEOUT_MS = 5 SUSPEND_TASK_AFTER_NUM_FAILURES = 6 ERROR_INTEGRATION = some_error_integration COPY GRANTS COMMENT = 'some comment' AFTER %s TAG (%s = 'v1') WHEN SYSTEM$STREAM_HAS_DATA('MYSTREAM') AS SELECT CURRENT_TIMESTAMP", id.FullyQualifiedName(), warehouseId.FullyQualifiedName(), otherTaskId.FullyQualifiedName(), tagId.FullyQualifiedName())
})
}

Expand Down
57 changes: 57 additions & 0 deletions pkg/sdk/testint/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package testint

import (
"context"
"database/sql"
"errors"
"fmt"
"path/filepath"
Expand Down Expand Up @@ -461,6 +462,19 @@ func createMaskingPolicy(t *testing.T, client *sdk.Client, database *sdk.Databas
return createMaskingPolicyWithOptions(t, client, database, schema, signature, sdk.DataTypeVARCHAR, expression, &sdk.CreateMaskingPolicyOptions{})
}

func createMaskingPolicyIdentity(t *testing.T, client *sdk.Client, database *sdk.Database, schema *sdk.Schema, columnType sdk.DataType) (*sdk.MaskingPolicy, func()) {
t.Helper()
name := "a"
signature := []sdk.TableColumnSignature{
{
Name: name,
Type: columnType,
},
}
expression := "a"
return createMaskingPolicyWithOptions(t, client, database, schema, signature, columnType, expression, &sdk.CreateMaskingPolicyOptions{})
}

func createMaskingPolicyWithOptions(t *testing.T, client *sdk.Client, database *sdk.Database, schema *sdk.Schema, signature []sdk.TableColumnSignature, returns sdk.DataType, expression string, options *sdk.CreateMaskingPolicyOptions) (*sdk.MaskingPolicy, func()) {
t.Helper()
var databaseCleanup func()
Expand Down Expand Up @@ -714,3 +728,46 @@ func createApplication(t *testing.T, client *sdk.Client, name string, packageNam
require.NoError(t, err)
}
}

func createRowAccessPolicy(t *testing.T, client *sdk.Client, schema *sdk.Schema) (sdk.SchemaObjectIdentifier, func()) {
t.Helper()
ctx := context.Background()
id := sdk.NewSchemaObjectIdentifier(schema.DatabaseName, schema.Name, random.String())
_, err := client.ExecForTests(ctx, fmt.Sprintf(`CREATE ROW ACCESS POLICY %s AS (A NUMBER) RETURNS BOOLEAN -> TRUE`, id.FullyQualifiedName()))
require.NoError(t, err)

return id, func() {
_, err := client.ExecForTests(ctx, fmt.Sprintf(`DROP ROW ACCESS POLICY %s`, id.FullyQualifiedName()))
require.NoError(t, err)
}
}

// TODO: extract getting row access policies as resource (like getting tag in system functions)
// getRowAccessPolicyFor is based on https://docs.snowflake.com/en/user-guide/security-row-intro#obtain-database-objects-with-a-row-access-policy.
func getRowAccessPolicyFor(t *testing.T, client *sdk.Client, id sdk.SchemaObjectIdentifier, objectType sdk.ObjectType) (*policyReference, error) {
t.Helper()
ctx := context.Background()

s := &policyReference{}
policyReferencesId := sdk.NewSchemaObjectIdentifier(id.DatabaseName(), "INFORMATION_SCHEMA", "POLICY_REFERENCES")
err := client.QueryOneForTests(ctx, s, fmt.Sprintf(`SELECT * FROM TABLE(%s(REF_ENTITY_NAME => '%s', REF_ENTITY_DOMAIN => '%v'))`, policyReferencesId.FullyQualifiedName(), id.FullyQualifiedName(), objectType))

return s, err
}

type policyReference struct {
PolicyDb string `db:"POLICY_DB"`
PolicySchema string `db:"POLICY_SCHEMA"`
PolicyName string `db:"POLICY_NAME"`
PolicyKind string `db:"POLICY_KIND"`
RefDatabaseName string `db:"REF_DATABASE_NAME"`
RefSchemaName string `db:"REF_SCHEMA_NAME"`
RefEntityName string `db:"REF_ENTITY_NAME"`
RefEntityDomain string `db:"REF_ENTITY_DOMAIN"`
RefColumnName sql.NullString `db:"REF_COLUMN_NAME"`
RefArgColumnNames string `db:"REF_ARG_COLUMN_NAMES"`
TagDatabase sql.NullString `db:"TAG_DATABASE"`
TagSchema sql.NullString `db:"TAG_SCHEMA"`
TagName sql.NullString `db:"TAG_NAME"`
PolicyStatus string `db:"POLICY_STATUS"`
}
Loading

0 comments on commit ed079d3

Please sign in to comment.