Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/v4.0.0 #299

Merged
merged 10 commits into from
Oct 29, 2024
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file.

*The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).*

## [4.0.0] - TBD
### Added
- use of structlog for standard log outputs

### Changed
- Refactored the main cli.py to be more modular to aide in future development and testing.


## [3.7.0] - 2024-07-22
### Added
- Improved unit test coverage
Expand Down
2 changes: 2 additions & 0 deletions demo/basics_demo/V1.0.2__StoredProc.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use database {{ database_name }};
use schema {{ schema_name }};
-- This block of code executes in Visual Studio Code but fails in Schemachange.
-- Use the $$ ... $$ to mark the block and execute the code block successfully.
-- The comment from a community user help find the root cause.
Expand Down
3 changes: 3 additions & 0 deletions demo/provision/setup_schemachange_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ CREATE DATABASE ROLE IF NOT EXISTS DB_R;
CREATE DATABASE ROLE IF NOT EXISTS DB_W;
CREATE DATABASE ROLE IF NOT EXISTS DB_C;

GRANT CREATE SCHEMA ON DATABASE IDENTIFIER($TARGET_DB_NAME) TO DATABASE ROLE DB_C;

GRANT DATABASE ROLE DB_C TO ROLE IDENTIFIER($DEPLOY_ROLE);

CREATE DATABASE ROLE IF NOT EXISTS IDENTIFIER($SC_M);
Expand All @@ -38,6 +40,7 @@ GRANT DATABASE ROLE IDENTIFIER($SC_W) TO DATABASE ROLE IDENTIFIER($SC_C);
CREATE SCHEMA IF NOT EXISTS IDENTIFIER($TARGET_SCHEMA_NAME) WITH MANAGED ACCESS;
-- USE SCHEMA INFORMATION_SCHEMA;
-- DROP SCHEMA IF EXISTS PUBLIC;
GRANT OWNERSHIP ON SCHEMA IDENTIFIER($TARGET_SCHEMA_NAME) TO ROLE IDENTIFIER($DEPLOY_ROLE);

USE SCHEMA IDENTIFIER($SCHEMACHANGE_NAMESPACE);
-- SCHEMA
Expand Down
1 change: 1 addition & 0 deletions demo/setup/basics_demo/A__setup_basics_demo.sql
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ GRANT DATABASE ROLE IDENTIFIER($SC_W) TO DATABASE ROLE IDENTIFIER($SC_C);
CREATE SCHEMA IF NOT EXISTS IDENTIFIER($TARGET_SCHEMA_NAME) WITH MANAGED ACCESS;
-- USE SCHEMA INFORMATION_SCHEMA;
-- DROP SCHEMA IF EXISTS PUBLIC;
GRANT OWNERSHIP ON SCHEMA IDENTIFIER($TARGET_SCHEMA_NAME) TO ROLE IDENTIFIER($DEPLOY_ROLE);

USE SCHEMA IDENTIFIER($SCHEMACHANGE_NAMESPACE);
-- SCHEMA
Expand Down
1 change: 1 addition & 0 deletions demo/setup/citibike_demo/A__setup_citibike_demo.sql
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ GRANT DATABASE ROLE IDENTIFIER($SC_W) TO DATABASE ROLE IDENTIFIER($SC_C);
CREATE SCHEMA IF NOT EXISTS IDENTIFIER($TARGET_SCHEMA_NAME) WITH MANAGED ACCESS;
-- USE SCHEMA INFORMATION_SCHEMA;
-- DROP SCHEMA IF EXISTS PUBLIC;
GRANT OWNERSHIP ON SCHEMA IDENTIFIER($TARGET_SCHEMA_NAME) TO ROLE IDENTIFIER($DEPLOY_ROLE);

USE SCHEMA IDENTIFIER($SCHEMACHANGE_NAMESPACE);
-- SCHEMA
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ GRANT DATABASE ROLE IDENTIFIER($SC_W) TO DATABASE ROLE IDENTIFIER($SC_C);
CREATE SCHEMA IF NOT EXISTS IDENTIFIER($TARGET_SCHEMA_NAME) WITH MANAGED ACCESS;
-- USE SCHEMA INFORMATION_SCHEMA;
-- DROP SCHEMA IF EXISTS PUBLIC;
GRANT OWNERSHIP ON SCHEMA IDENTIFIER($TARGET_SCHEMA_NAME) TO ROLE IDENTIFIER($DEPLOY_ROLE);

USE SCHEMA IDENTIFIER($SCHEMACHANGE_NAMESPACE);
-- SCHEMA
Expand Down
2 changes: 1 addition & 1 deletion schemachange/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

# region Global Variables
# metadata
SCHEMACHANGE_VERSION = "3.7.0"
SCHEMACHANGE_VERSION = "4.0.0"
SNOWFLAKE_APPLICATION_NAME = "schemachange"
module_logger = structlog.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion schemachange/config/BaseConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
T = TypeVar("T", bound="BaseConfig")


@dataclasses.dataclass(frozen=True, kw_only=True)
@dataclasses.dataclass(frozen=True)
class BaseConfig(ABC):
default_config_file_name: ClassVar[str] = "schemachange-config.yml"

Expand Down
2 changes: 1 addition & 1 deletion schemachange/config/DeployConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from schemachange.config.utils import get_snowflake_identifier_string


@dataclasses.dataclass(frozen=True, kw_only=True)
@dataclasses.dataclass(frozen=True)
class DeployConfig(BaseConfig):
subcommand: Literal["deploy"] = "deploy"
snowflake_account: str | None = None
Expand Down
10 changes: 8 additions & 2 deletions schemachange/config/RenderConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
from schemachange.config.utils import validate_file_path


@dataclasses.dataclass(frozen=True, kw_only=True)
@dataclasses.dataclass(frozen=True)
class RenderConfig(BaseConfig):
script_path: Path | None = None
subcommand: Literal["render"] = "render"
script_path: Path

@classmethod
def factory(
Expand All @@ -31,3 +31,9 @@ def factory(
script_path=validate_file_path(file_path=script_path),
**kwargs,
)

def __post_init__(self):
if self.script_path is None:
raise TypeError(
"RenderConfig is missing 1 required argument: 'script_path'"
)
8 changes: 5 additions & 3 deletions schemachange/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ def alphanum_convert(text: str):
# Each number is converted to and integer and string parts are left as strings
# This will enable correct sorting in python when the lists are compared
# e.g. get_alphanum_key('1.2.2') results in ['', 1, '.', 2, '.', 2, '']
def get_alphanum_key(key):
def get_alphanum_key(key: str | int | None) -> list:
if key == "" or key is None:
return []
alphanum_key = [alphanum_convert(c) for c in re.split("([0-9]+)", key)]
return alphanum_key

Expand Down Expand Up @@ -100,7 +102,7 @@ def deploy(config: DeployConfig, session: SnowflakeSession):
script_metadata = versioned_scripts.get(script.name)

if (
max_published_version != ""
max_published_version is not None
and get_alphanum_key(script.version) <= max_published_version
):
if script_metadata is None:
Expand All @@ -113,7 +115,7 @@ def deploy(config: DeployConfig, session: SnowflakeSession):
else:
script_log.debug(
"Script has already been applied",
max_published_version=str(max_published_version),
max_published_version=max_published_version,
)
if script_metadata["checksum"] != checksum_current:
script_log.info("Script checksum has drifted since application")
Expand Down
34 changes: 14 additions & 20 deletions schemachange/session/Credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import dataclasses
import os
from abc import ABC
from typing import Literal, Union

import structlog
Expand All @@ -14,37 +13,32 @@
)


@dataclasses.dataclass(kw_only=True, frozen=True)
class Credential(ABC):
authenticator: str


@dataclasses.dataclass(kw_only=True, frozen=True)
class OauthCredential(Credential):
authenticator: Literal["oauth"] = "oauth"
@dataclasses.dataclass(frozen=True)
class OauthCredential:
token: str
authenticator: Literal["oauth"] = "oauth"


@dataclasses.dataclass(kw_only=True, frozen=True)
class PasswordCredential(Credential):
authenticator: Literal["snowflake"] = "snowflake"
@dataclasses.dataclass(frozen=True)
class PasswordCredential:
password: str
authenticator: Literal["snowflake"] = "snowflake"


@dataclasses.dataclass(kw_only=True, frozen=True)
class PrivateKeyCredential(Credential):
authenticator: Literal["snowflake"] = "snowflake"
@dataclasses.dataclass(frozen=True)
class PrivateKeyCredential:
private_key: bytes
authenticator: Literal["snowflake"] = "snowflake"


@dataclasses.dataclass(kw_only=True, frozen=True)
class ExternalBrowserCredential(Credential):
authenticator: Literal["externalbrowser"] = "externalbrowser"
@dataclasses.dataclass(frozen=True)
class ExternalBrowserCredential:
password: str | None = None
authenticator: Literal["externalbrowser"] = "externalbrowser"


@dataclasses.dataclass(kw_only=True, frozen=True)
class OktaCredential(Credential):
@dataclasses.dataclass(frozen=True)
class OktaCredential:
authenticator: str
password: str

Expand Down
8 changes: 4 additions & 4 deletions schemachange/session/Script.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
T = TypeVar("T", bound="Script")


@dataclasses.dataclass(kw_only=True, frozen=True)
@dataclasses.dataclass(frozen=True)
class Script(ABC):
pattern: ClassVar[Pattern[str]]
type: ClassVar[Literal["V", "R", "A"]]
Expand Down Expand Up @@ -47,7 +47,7 @@ def from_path(cls, file_path: Path, **kwargs) -> T:
)


@dataclasses.dataclass(kw_only=True, frozen=True)
@dataclasses.dataclass(frozen=True)
class VersionedScript(Script):
pattern: ClassVar[re.Pattern[str]] = re.compile(
r"^(V)(?P<version>.+?)?__(?P<description>.+?)\.", re.IGNORECASE
Expand All @@ -64,15 +64,15 @@ def from_path(cls: T, file_path: Path, **kwargs) -> T:
)


@dataclasses.dataclass(kw_only=True, frozen=True)
@dataclasses.dataclass(frozen=True)
class RepeatableScript(Script):
pattern: ClassVar[re.Pattern[str]] = re.compile(
r"^(R)__(?P<description>.+?)\.", re.IGNORECASE
)
type: ClassVar[Literal["R"]] = "R"


@dataclasses.dataclass(kw_only=True, frozen=True)
@dataclasses.dataclass(frozen=True)
class AlwaysScript(Script):
pattern: ClassVar[re.Pattern[str]] = re.compile(
r"^(A)__(?P<description>.+?)\.", re.IGNORECASE
Expand Down
10 changes: 3 additions & 7 deletions schemachange/session/SnowflakeSession.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,7 @@ def get_script_metadata(

self.logger.info(
"Max applied change script version %(max_published_version)s"
% {
"max_published_version": max_published_version
if max_published_version != ""
else "None"
}
% {"max_published_version": max_published_version}
)
return change_history, r_scripts_checksum, max_published_version

Expand Down Expand Up @@ -251,10 +247,10 @@ def fetch_versioned_scripts(

# Collect all the results into a list
versioned_scripts: dict[str, dict[str, str | int]] = defaultdict(dict)
versions: list[str | int] = []
versions: list[str | int | None] = []
for cursor in results:
for version, script, checksum in cursor:
versions.append(version)
versions.append(version if version != "" else None)
versioned_scripts[script] = {
"version": version,
"script": script,
Expand Down
8 changes: 6 additions & 2 deletions tests/test_cli_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@


def test_cli_given__schemachange_version_change_updated_in_setup_config_file():
assert SCHEMACHANGE_VERSION == "3.7.0"
assert SCHEMACHANGE_VERSION == "4.0.0"


def test_cli_given__constants_exist():
Expand All @@ -30,7 +30,11 @@ def test_alphanum_convert_given__lowercase():


def test_get_alphanum_key_given__empty_string():
assert get_alphanum_key("") == [""]
assert get_alphanum_key("") == []


def test_get_alphanum_key_given__none():
assert get_alphanum_key(None) == []


def test_get_alphanum_key_given__numbers_only():
Expand Down