From 78fe3306d69224ec1e7497620624e7d0c2b9e81b Mon Sep 17 00:00:00 2001 From: Peter Kosztolanyi Date: Sun, 2 Jun 2019 16:39:43 +0100 Subject: [PATCH] initial commit --- .gitignore | 32 + LICENSE | 9 + README.md | 170 +++++ flow-diagram.jpg | Bin 0 -> 157126 bytes flow-diagram.svg | 1 + requirements.txt | 6 + setup.py | 27 + target_snowflake/__init__.py | 266 ++++++++ target_snowflake/db_sync.py | 598 ++++++++++++++++++ tests/integration/resources/invalid-json.json | 4 + .../resources/invalid-message-order.json | 3 + .../messages-with-multi-schemas.json | 26 + ...messages-with-non-db-friendly-columns.json | 11 + .../messages-with-three-streams.json | 25 + .../messages-with-unicode-characters.json | 12 + tests/integration/test_target_snowflake.py | 286 +++++++++ tests/integration/utils.py | 60 ++ tests/unit/test_unit.py | 90 +++ 18 files changed, 1626 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 flow-diagram.jpg create mode 100644 flow-diagram.svg create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 target_snowflake/__init__.py create mode 100644 target_snowflake/db_sync.py create mode 100644 tests/integration/resources/invalid-json.json create mode 100644 tests/integration/resources/invalid-message-order.json create mode 100644 tests/integration/resources/messages-with-multi-schemas.json create mode 100644 tests/integration/resources/messages-with-non-db-friendly-columns.json create mode 100644 tests/integration/resources/messages-with-three-streams.json create mode 100644 tests/integration/resources/messages-with-unicode-characters.json create mode 100644 tests/integration/test_target_snowflake.py create mode 100644 tests/integration/utils.py create mode 100644 tests/unit/test_unit.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2661658 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# IDE +.vscode +.idea/* + + +# Python +__pycache__/ +*.py[cod] +*$py.class +.virtualenvs +*.egg-info/ +*__pycache__/ +*~ +dist/ + +# Singer JSON files +properties.json +config.json +state.json + +*.db +.DS_Store +venv +env +blog_old.md +node_modules +*.pyc +tmp + +# Docs +docs/_build/ +docs/_templates/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6675fc3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2019 TransferWise Ltd. (https://transferwise.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7b3512 --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# pipelinewise-target-snowflake + +[![PyPI version](https://badge.fury.io/py/pipelinewise-target-snowflake.svg)](https://badge.fury.io/py/pipelinewise-target-snowflake) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pipelinewise-target-snowflake.svg)](https://pypi.org/project/pipelinewise-target-snowflake/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +[Singer](https://www.singer.io/) target that loads data into Snowflake following the [Singer spec](https://github.com/singer-io/getting-started/blob/master/docs/SPEC.md). + +This is a [PipelineWise](https://transferwise.github.io/pipelinewise) compatible target connector. + +## How to use it + +The recommended method of running this target is to use it from [PipelineWise](https://transferwise.github.io/pipelinewise). When running it from PipelineWise you don't need to configure this tap with JSON files and most of things are automated. Please check the related documentation at [Target Snowflake](https://transferwise.github.io/pipelinewise/connectors/targets/snowflake.html) + +If you want to run this [Singer Target](https://singer.io) independently please read further. + +## Install + +First, make sure Python 3 is installed on your system or follow these +installation instructions for [Mac](http://docs.python-guide.org/en/latest/starting/install3/osx/) or +[Ubuntu](https://www.digitalocean.com/community/tutorials/how-to-install-python-3-and-set-up-a-local-programming-environment-on-ubuntu-16-04). + +It's recommended to use a virtualenv: + +```bash + python3 -m venv venv + pip install pipelinewise-target-snowflake +``` + +or + +```bash + python3 -m venv venv + . venv/bin/activate + pip install --upgrade pip + pip install . +``` + +## Flow diagram + +![Flow Diagram](flow-diagram.jpg) + +### To run + +Like any other target that's following the singer specificiation: + +`some-singer-tap | target-snowflake --config [config.json]` + +It's reading incoming messages from STDIN and using the properites in `config.json` to upload data into Snowflake. + +**Note**: To avoid version conflicts run `tap` and `targets` in separate virtual environments. + +### Pre-requirements + +You need to create two objects in snowflake in one schema before start using this target. + +1. A named external stage object on S3. This will be used to upload the CSV files to S3 and to MERGE data into snowflake tables. + +``` +CREATE STAGE {schema}.{stage_name} +url='s3://{s3_bucket}' +credentials=(AWS_KEY_ID='{aws_key_id}' AWS_SECRET_KEY='{aws_secret_key}') +encryption=(MASTER_KEY='{client_side_encryption_master_key}'); +``` + +The `encryption` option is optional and used for client side encryption. If you want client side encryption enabled you'll need +to define the same master key in the target `config.json`. Furhter details below in the Configuration settings section. + +2. A named file format. This will be used by the MERGE/COPY commands to parse the CSV files correctly from S3: + +`CREATE file format IF NOT EXISTS {schema}.{file_format_name} type = 'CSV' escape='\\' field_optionally_enclosed_by='"';` + + +### Configuration settings + +Running the the target connector requires a `config.json` file. Example with the minimal settings: + + ```json + { + "account": "localhost", + "dbname": 5432, + "user": "my_analytics", + "password": "my_user", + "warehouse": "my_virtual_warehouse", + "aws_access_key_id": "secret", + "aws_secret_access_key": "secret", + "s3_bucket": "bucket_name", + "stage": "snowflake_external_stage_object_name", + "file_format": "snowflake_file_format_object_name", + "default_target_schema": "my_target_schema" + } + ``` + +Full list of options in `config.json`: + +| Property | Type | Required? | Description | +|-------------------------------------|---------|------------|---------------------------------------------------------------| +| account | String | Yes | Snowflake account name (i.e. rtXXXXX.eu-central-1) | +| dbname | String | Yes | Snowflake Database name | +| user | String | Yes | Snowflake User | +| password | String | Yes | Snowflake Password | +| warehouse | String | Yes | Snowflake virtual warehouse name | +| aws_access_key_id | String | Yes | S3 Access Key Id | +| aws_secret_access_key | String | Yes | S3 Secret Access Key | +| s3_bucket | String | Yes | S3 Bucket name | +| s3_key_prefix | String | | (Default: None) A static prefix before the generated S3 key names. Using prefixes you can upload files into specific directories in the S3 bucket. | +| stage | String | Yes | Named external stage name created at pre-requirements section. Has to be a fully qualified name including the schema name | +| file_format | String | Yes | Named file format name created at pre-requirements section. Has to be a fully qualified name including the schema name. | +| batch_size | Integer | | (Default: 100000) Maximum number of rows in each batch. At the end of each batch, the rows in the batch are loaded into Snowflake. | +| default_target_schema | String | | Name of the schema where the tables will be created. If `schema_mapping` is not defined then every stream sent by the tap is loaded into this schema. | +| default_target_schema_select_permission | String | | Grant USAGE privilege on newly created schemas and grant SELECT privilege on newly created tables to a specific role or a list of roles. If `schema_mapping` is not defined then every stream sent by the tap is granted accordingly. | +| schema_mapping | Object | | Useful if you want to load multiple streams from one tap to multiple Snowflake schemas.

If the tap sends the `stream_id` in `-` format then this option overwrites the `default_target_schema` value. Note, that using `schema_mapping` you can overwrite the `default_target_schema_select_permission` value to grant SELECT permissions to different groups per schemas or optionally you can create indices automatically for the replicated tables.

**Note**: This is an experimental feature and recommended to use via PipelineWise YAML files that will generate the object mapping in the right JSON format. For further info check a [PipelineWise YAML Example] +| disable_table_cache | Boolean | | (Default: False) By default the connector caches the available table structures in Snowflake at startup. In this way it doesn't need to run additional queries when ingesting data to check if altering the target tables is required. With `disable_table_cache` option you can turn off this caching. You will always see the most recent table structures but will cause an extra query runtime. | +| client_side_encryption_master_key | String | | (Default: None) When this is defined, Client-Side Encryption is enabled. The data in S3 will be encrypted, No third parties, including Amazon AWS and any ISPs, can see data in the clear. Snowflake COPY command will decrypt the data once it's in Snowflake. The master key must be 256-bit length and must be encoded as base64 string. | +| client_side_encryption_stage_object | String | | (Default: None) Required when `client_side_encryption_master_key` is defined. The name of the encrypted stage object in Snowflake that created separately and using the same encryption master key. | +| add_metadata_columns | Boolean | | (Default: False) Metadata columns add extra row level information about data ingestions, (i.e. when was the row read in source, when was inserted or deleted in snowflake etc.) Metadata columns are creating automatically by adding extra columns to the tables with a column prefix `_SDC_`. The column names are following the stitch naming conventions documented at https://www.stitchdata.com/docs/data-structure/integration-schemas#sdc-columns. Enabling metadata columns will flag the deleted rows by setting the `_SDC_DELETED_AT` metadata column. Without the `add_metadata_columns` option the deleted rows from singer taps will not be recongisable in Snowflake. | +| hard_delete | Boolean | | (Default: False) When `hard_delete` option is true then DELETE SQL commands will be performed in Snowflake to delete rows in tables. It's achieved by continuously checking the `_SDC_DELETED_AT` metadata column sent by the singer tap. Due to deleting rows requires metadata columns, `hard_delete` option automatically enables the `add_metadata_columns` option as well. | + + + +### To run tests: + +1. Define environment variables that requires running the tests +``` + export TARGET_SNOWFLAKE_ACCOUNT= + export TARGET_SNOWFLAKE_DBNAME= + export TARGET_SNOWFLAKE_USER= + export TARGET_SNOWFLAKE_PASSWORD= + export TARGET_SNOWFLAKE_WAREHOUSE= + export TARGET_SNOWFLAKE_SCHEMA= + export TARGET_SNOWFLAKE_AWS_ACCESS_KEY= + export TARGET_SNOWFLAKE_AWS_SECRET_ACCESS_KEY= + export TARGET_SNOWFLAKE_S3_BUCKET= + export TARGET_SNOWFLAKE_S3_KEY_PREFIX= + export TARGET_SNOWFLAKE_STAGE= + export TARGET_SNOWFLAKE_FILE_FORMAT= + export CLIENT_SIDE_ENCRYPTION_MASTER_KEY= + export CLIENT_SIDE_ENCRYPTION_STAGE_OBJECT= +``` + +2. Install python dependencies in a virtual env and run nose unit and integration tests +``` + python3 -m venv venv + . venv/bin/activate + pip install --upgrade pip + pip install . + pip install nose +``` + +3. To run unit tests: +``` + nosetests --where=tests/unit +``` + +4. To run integration tests: +``` + nosetests --where=tests/integration +``` + +### To run pylint: + +1. Install python dependencies and run python linter +``` + python3 -m venv venv + . venv/bin/activate + pip install --upgrade pip + pip install . + pip install pylint + pylint target_snowflake -d C,W,unexpected-keyword-arg,duplicate-code +``` diff --git a/flow-diagram.jpg b/flow-diagram.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5db8138604e45fc25adf9146d550a737b2e4950c GIT binary patch literal 157126 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 z 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 literal 0 HcmV?d00001 diff --git a/flow-diagram.svg b/flow-diagram.svg new file mode 100644 index 0000000..a888b8d --- /dev/null +++ b/flow-diagram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e0799a8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +idna==2.7 +singer-python==5.1.1 +snowflake-connector-python==1.7.4 +boto3==1.9.33 +inflection==0.3.1 +joblib==0.13.2 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ad0cdd3 --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +from setuptools import setup + +setup(name="pipelinewise-target-snowflake", + version="1.0.0", + description="Singer.io target for loading data to Snowflake - PipelineWise compatible", + author="TransferWise", + url='https://github.com/transferwise/pipelinewise-target-snowflake', + classifiers=["Programming Language :: Python :: 3 :: Only"], + py_modules=["target_snowflake"], + install_requires=[ + 'idna==2.7', + 'singer-python==5.1.1', + 'snowflake-connector-python==1.7.4', + 'boto3==1.9.33', + 'inflection==0.3.1', + 'joblib==0.13.2' + ], + entry_points=""" + [console_scripts] + target-snowflake=target_snowflake:main + """, + packages=["target_snowflake"], + package_data = {}, + include_package_data=True, +) \ No newline at end of file diff --git a/target_snowflake/__init__.py b/target_snowflake/__init__.py new file mode 100644 index 0000000..b08274e --- /dev/null +++ b/target_snowflake/__init__.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 + +import argparse +import io +import os +import sys +import json +import threading +import http.client +import urllib +from datetime import datetime +import time +import collections +from tempfile import NamedTemporaryFile +from decimal import Decimal +from joblib import Parallel, delayed, parallel_backend +import tempfile + +import pkg_resources +from jsonschema import ValidationError, Draft4Validator, FormatChecker +import singer +from target_snowflake.db_sync import DbSync + +logger = singer.get_logger() + +def float_to_decimal(value): + '''Walk the given data structure and turn all instances of float into + double.''' + if isinstance(value, float): + return Decimal(str(value)) + if isinstance(value, list): + return [float_to_decimal(child) for child in value] + if isinstance(value, dict): + return {k: float_to_decimal(v) for k, v in value.items()} + return value + +def add_metadata_columns_to_schema(schema_message): + """Metadata _sdc columns according to the stitch documentation at + https://www.stitchdata.com/docs/data-structure/integration-schemas#sdc-columns + + Metadata columns gives information about data injections + """ + extended_schema_message = schema_message + extended_schema_message['schema']['properties']['_sdc_batched_at'] = { 'type': ['null', 'string'], 'format': 'date-time' } + extended_schema_message['schema']['properties']['_sdc_deleted_at'] = { 'type': ['null', 'string'] } + extended_schema_message['schema']['properties']['_sdc_extracted_at'] = { 'type': ['null', 'string'], 'format': 'date-time' } + extended_schema_message['schema']['properties']['_sdc_primary_key'] = {'type': ['null', 'string'] } + extended_schema_message['schema']['properties']['_sdc_received_at'] = { 'type': ['null', 'string'], 'format': 'date-time' } + extended_schema_message['schema']['properties']['_sdc_sequence'] = {'type': ['integer'] } + extended_schema_message['schema']['properties']['_sdc_table_version'] = {'type': ['null', 'string'] } + + return extended_schema_message + +def add_metadata_values_to_record(record_message, stream_to_sync): + """Populate metadata _sdc columns from incoming record message + The location of the required attributes are fixed in the stream + """ + extended_record = record_message['record'] + extended_record['_sdc_batched_at'] = datetime.now().isoformat() + extended_record['_sdc_deleted_at'] = record_message.get('record', {}).get('_sdc_deleted_at') + extended_record['_sdc_extracted_at'] = record_message.get('time_extracted') + extended_record['_sdc_primary_key'] = stream_to_sync.stream_schema_message['key_properties'] + extended_record['_sdc_received_at'] = datetime.now().isoformat() + extended_record['_sdc_sequence'] = int(round(time.time() * 1000)) + extended_record['_sdc_table_version'] = record_message.get('version') + + return extended_record + +def emit_state(state): + if state is not None: + line = json.dumps(state) + logger.debug('Emitting state {}'.format(line)) + sys.stdout.write("{}\n".format(line)) + sys.stdout.flush() + +def get_schema_names_from_config(config): + default_target_schema = config.get('default_target_schema') + schema_mapping = config.get('schema_mapping', {}) + schema_names = [] + + if default_target_schema: + schema_names.append(default_target_schema) + + if schema_mapping: + for source_schema, target in schema_mapping.items(): + schema_names.append(target.get('target_schema')) + + return schema_names + +# pylint: disable=too-many-locals,too-many-branches,too-many-statements +def persist_lines(config, lines): + state = None + schemas = {} + key_properties = {} + validators = {} + records_to_load = {} + csv_files_to_load = {} + row_count = {} + stream_to_sync = {} + batch_size_rows = config.get('batch_size_rows', 100000) + table_columns_cache = None + + # Cache the available schemas, tables and columns from snowflake if not disabled in config + # The cache will be used later use to avoid lot of small queries hitting snowflake + if not ('disable_table_cache' in config and config['disable_table_cache'] == True): + logger.info("Caching available catalog objects in snowflake...") + filter_schemas = get_schema_names_from_config(config) + table_columns_cache = DbSync(config).get_table_columns(filter_schemas=filter_schemas) + + # Loop over lines from stdin + for line in lines: + try: + o = json.loads(line) + except json.decoder.JSONDecodeError: + logger.error("Unable to parse:\n{}".format(line)) + raise + + if 'type' not in o: + raise Exception("Line is missing required key 'type': {}".format(line)) + t = o['type'] + + if t == 'RECORD': + if 'stream' not in o: + raise Exception("Line is missing required key 'stream': {}".format(line)) + if o['stream'] not in schemas: + raise Exception( + "A record for stream {} was encountered before a corresponding schema".format(o['stream'])) + + # Get schema for this record's stream + stream = o['stream'] + + # Validate record + try: + validators[stream].validate(float_to_decimal(o['record'])) + except Exception as ex: + if type(ex).__name__ == "InvalidOperation": + logger.error("Data validation failed and cannot load to destination. RECORD: {}\n'multipleOf' validations that allows long precisions are not supported (i.e. with 15 digits or more). Try removing 'multipleOf' methods from JSON schema." + .format(o['record'])) + raise ex + + primary_key_string = stream_to_sync[stream].record_primary_key_string(o['record']) + if not primary_key_string: + primary_key_string = 'RID-{}'.format(row_count[stream]) + + if stream not in records_to_load: + records_to_load[stream] = {} + + if config.get('add_metadata_columns') or config.get('hard_delete'): + records_to_load[stream][primary_key_string] = add_metadata_values_to_record(o, stream_to_sync[stream]) + else: + records_to_load[stream][primary_key_string] = o['record'] + + row_count[stream] = len(records_to_load[stream]) + + if row_count[stream] >= batch_size_rows: + flush_records(stream, records_to_load[stream], row_count[stream], stream_to_sync[stream]) + row_count[stream] = 0 + records_to_load[stream] = {} + + state = None + elif t == 'STATE': + logger.debug('Setting state to {}'.format(o['value'])) + state = o['value'] + elif t == 'SCHEMA': + if 'stream' not in o: + raise Exception("Line is missing required key 'stream': {}".format(line)) + stream = o['stream'] + + schemas[stream] = o + schema = float_to_decimal(o['schema']) + validators[stream] = Draft4Validator(schema, format_checker=FormatChecker()) + + # flush records from previous stream SCHEMA + if row_count.get(stream, 0) > 0: + flush_records(stream, records_to_load[stream], row_count[stream], stream_to_sync[stream]) + + # key_properties key must be available in the SCHEMA message. + if 'key_properties' not in o: + raise Exception("key_properties field is required") + + # Log based and Incremental replications on tables with no Primary Key + # cause duplicates when merging UPDATE events. + # Stop loading data by default if no Primary Key. + # + # If you want to load tables with no Primary Key: + # 1) Set ` 'primary_key_required': false ` in the target-snowflake config.json + # or + # 2) Use fastsync [postgres-to-snowflake, mysql-to-snowflake, etc.] + if config.get('primary_key_required', True) and len(o['key_properties']) == 0: + logger.critical("Primary key is set to mandatory but not defined in the [{}] stream".format(stream)) + raise Exception("key_properties field is required") + + key_properties[stream] = o['key_properties'] + + if config.get('add_metadata_columns') or config.get('hard_delete'): + stream_to_sync[stream] = DbSync(config, add_metadata_columns_to_schema(o)) + else: + stream_to_sync[stream] = DbSync(config, o) + + stream_to_sync[stream].create_schema_if_not_exists(table_columns_cache) + stream_to_sync[stream].sync_table(table_columns_cache) + row_count[stream] = 0 + csv_files_to_load[stream] = NamedTemporaryFile(mode='w+b') + elif t == 'ACTIVATE_VERSION': + logger.debug('ACTIVATE_VERSION message') + else: + raise Exception("Unknown message type {} in message {}" + .format(o['type'], o)) + + + # Single-host, thread-based parallelism + with parallel_backend('threading', n_jobs=-1): + Parallel()(delayed(load_stream_batch)( + stream=stream, + records_to_load=records_to_load[stream], + row_count=row_count[stream], + db_sync=stream_to_sync[stream], + delete_rows=config.get('hard_delete') + ) for (stream) in records_to_load.keys()) + + return state + + +def load_stream_batch(stream, records_to_load, row_count, db_sync, delete_rows=False): + #Load into snowflake + if row_count > 0: + flush_records(stream, records_to_load, row_count, db_sync) + + # Delete soft-deleted, flagged rows - where _sdc_deleted at is not null + if delete_rows: + db_sync.delete_rows(stream) + + +def flush_records(stream, records_to_load, row_count, db_sync): + csv_fd, csv_file = tempfile.mkstemp() + with open(csv_fd, 'w+b') as f: + for record in records_to_load.values(): + csv_line = db_sync.record_to_csv_line(record) + f.write(bytes(csv_line + '\n', 'UTF-8')) + + s3_key = db_sync.put_to_stage(csv_file, stream, row_count) + db_sync.load_csv(s3_key, row_count) + os.remove(csv_file) + db_sync.delete_from_stage(s3_key) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--config', help='Config file') + args = parser.parse_args() + + if args.config: + with open(args.config) as input: + config = json.load(input) + else: + config = {} + + input = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') + state = persist_lines(config, input) + + emit_state(state) + logger.debug("Exiting normally") + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/target_snowflake/db_sync.py b/target_snowflake/db_sync.py new file mode 100644 index 0000000..6754507 --- /dev/null +++ b/target_snowflake/db_sync.py @@ -0,0 +1,598 @@ +import os +import json +import boto3 +import snowflake.connector +import singer +import collections +import inflection +import re +import itertools +import time +import datetime + +from snowflake.connector.encryption_util import SnowflakeEncryptionUtil +from snowflake.connector.remote_storage_util import SnowflakeFileEncryptionMaterial + +logger = singer.get_logger() + + +def validate_config(config): + errors = [] + required_config_keys = [ + 'account', + 'dbname', + 'user', + 'password', + 'warehouse', + 'aws_access_key_id', + 'aws_secret_access_key', + 's3_bucket', + 'stage', + 'file_format' + ] + + # Check if mandatory keys exist + for k in required_config_keys: + if not config.get(k, None): + errors.append("Required key is missing from config: [{}]".format(k)) + + # Check target schema config + config_default_target_schema = config.get('default_target_schema', None) + config_schema_mapping = config.get('schema_mapping', None) + if not config_default_target_schema and not config_schema_mapping: + errors.append("Neither 'default_target_schema' (string) nor 'schema_mapping' (object) keys set in config.") + + # Check client-side encryption config + config_cse_key = config.get('client_side_encryption_master_key', None) + + return errors + + +def column_type(schema_property): + property_type = schema_property['type'] + property_format = schema_property['format'] if 'format' in schema_property else None + column_type = 'text' + if 'object' in property_type or 'array' in property_type: + column_type = 'variant' + + # Every date-time JSON value is currently mapped to TIMESTAMP_NTZ + # + # TODO: Detect if timezone postfix exists in the JSON and find if TIMESTAMP_TZ or + # TIMSTAMP_NTZ is the better column type + elif property_format == 'date-time': + column_type = 'timestamp_ntz' + elif property_format == 'time': + column_type = 'time' + elif 'number' in property_type: + column_type = 'float' + elif 'integer' in property_type and 'string' in property_type: + column_type = 'text' + elif 'integer' in property_type: + column_type = 'number' + elif 'boolean' in property_type: + column_type = 'boolean' + + return column_type + + +def column_trans(schema_property): + property_type = schema_property['type'] + column_trans = '' + if 'object' in property_type or 'array' in property_type: + column_trans = 'parse_json' + + return column_trans + + +def safe_column_name(name): + return '"{}"'.format(name).upper() + + +def column_clause(name, schema_property): + return '{} {}'.format(safe_column_name(name), column_type(schema_property)) + + +def flatten_key(k, parent_key, sep): + full_key = parent_key + [k] + inflected_key = [n for n in full_key] + reducer_index = 0 + while len(sep.join(inflected_key)) >= 255 and reducer_index < len(inflected_key): + reduced_key = re.sub(r'[a-z]', '', inflection.camelize(inflected_key[reducer_index])) + inflected_key[reducer_index] = \ + (reduced_key if len(reduced_key) > 1 else inflected_key[reducer_index][0:3]).lower() + reducer_index += 1 + + return sep.join(inflected_key) + + +def flatten_schema(d, parent_key=[], sep='__'): + items = [] + for k, v in d['properties'].items(): + new_key = flatten_key(k, parent_key, sep) + if 'type' in v.keys(): + if 'object' in v['type']: + items.extend(flatten_schema(v, parent_key + [k], sep=sep).items()) + else: + items.append((new_key, v)) + else: + if len(v.values()) > 0: + if list(v.values())[0][0]['type'] == 'string': + list(v.values())[0][0]['type'] = ['null', 'string'] + items.append((new_key, list(v.values())[0][0])) + elif list(v.values())[0][0]['type'] == 'array': + list(v.values())[0][0]['type'] = ['null', 'array'] + items.append((new_key, list(v.values())[0][0])) + + key_func = lambda item: item[0] + sorted_items = sorted(items, key=key_func) + for k, g in itertools.groupby(sorted_items, key=key_func): + if len(list(g)) > 1: + raise ValueError('Duplicate column name produced in schema: {}'.format(k)) + + return dict(sorted_items) + + +def flatten_record(d, parent_key=[], sep='__'): + items = [] + for k, v in d.items(): + new_key = flatten_key(k, parent_key, sep) + if isinstance(v, collections.MutableMapping): + items.extend(flatten_record(v, parent_key + [k], sep=sep).items()) + else: + items.append((new_key, json.dumps(v) if type(v) is list else v)) + return dict(items) + + +def primary_column_names(stream_schema_message): + return [safe_column_name(p) for p in stream_schema_message['key_properties']] + +def stream_name_to_dict(stream_name): + schema_name = None + table_name = stream_name + + # Schema and table name can be derived from stream if it's in - format + s = stream_name.split('-') + if len(s) > 1: + schema_name = s[0] + table_name = '_'.join(s[1:]) + + return { + 'schema_name': schema_name, + 'table_name': table_name + } + +# pylint: disable=too-many-public-methods +class DbSync: + def __init__(self, connection_config, stream_schema_message=None): + """ + connection_config: Snowflake connection details + + stream_schema_message: An instance of the DbSync class is typically used to load + data only from a certain singer tap stream. + + The stream_schema_message holds the destination schema + name and the JSON schema that will be used to + validate every RECORDS messages that comes from the stream. + Schema validation happening before creating CSV and before + uploading data into Snowflake. + + If stream_schema_message is not defined that we can use + the DbSync instance as a generic purpose connection to + Snowflake and can run individual queries. For example + collecting catalog informations from Snowflake for caching + purposes. + """ + self.connection_config = connection_config + config_errors = validate_config(connection_config) + if len(config_errors) == 0: + self.connection_config = connection_config + else: + logger.error("Invalid configuration:\n * {}".format('\n * '.join(config_errors))) + exit(1) + + self.schema_name = None + self.grantees = None + if stream_schema_message is not None: + # Define target schema name. + # -------------------------- + # Target schema name can be defined in multiple ways: + # + # 1: 'default_target_schema' key : Target schema is the same for every incoming stream if + # not specified explicitly for a given stream in + # the `schema_mapping` object + # 2: 'schema_mapping' key : Target schema defined explicitly for a given stream. + # Example config.json: + # "schema_mapping": { + # "my_tap_stream_id": { + # "target_schema": "my_snowflake_schema", + # "target_schema_select_permissions": [ "role_with_select_privs" ] + # } + # } + config_default_target_schema = self.connection_config.get('default_target_schema', '').strip() + config_schema_mapping = self.connection_config.get('schema_mapping', {}) + + stream_name = stream_schema_message['stream'] + stream_schema_name = stream_name_to_dict(stream_name)['schema_name'] + if config_schema_mapping and stream_schema_name in config_schema_mapping: + self.schema_name = config_schema_mapping[stream_schema_name].get('target_schema') + elif config_default_target_schema: + self.schema_name = config_default_target_schema + + if not self.schema_name: + raise Exception("Target schema name not defined in config. Neither 'default_target_schema' (string) nor 'schema_mapping' (object) defines target schema for {} stream.".format(stream_name)) + + # Define grantees + # --------------- + # Grantees can be defined in multiple ways: + # + # 1: 'default_target_schema_select_permissions' key : USAGE and SELECT privileges will be granted on every table to a given role + # for every incoming stream if not specified explicitly + # in the `schema_mapping` object + # 2: 'target_schema_select_permissions' key : Roles to grant USAGE and SELECT privileges defined explicitly + # for a given stream. + # Example config.json: + # "schema_mapping": { + # "my_tap_stream_id": { + # "target_schema": "my_snowflake_schema", + # "target_schema_select_permissions": [ "role_with_select_privs" ] + # } + # } + self.grantees = self.connection_config.get('default_target_schema_select_permissions') + if config_schema_mapping and stream_schema_name in config_schema_mapping: + self.grantees = config_schema_mapping[stream_schema_name].get('target_schema_select_permissions', self.grantees) + + self.stream_schema_message = stream_schema_message + + if stream_schema_message is not None: + self.flatten_schema = flatten_schema(stream_schema_message['schema']) + + self.s3 = boto3.client( + 's3', + aws_access_key_id=self.connection_config['aws_access_key_id'], + aws_secret_access_key=self.connection_config['aws_secret_access_key'] + ) + + + def open_connection(self): + return snowflake.connector.connect( + user=self.connection_config['user'], + password=self.connection_config['password'], + account=self.connection_config['account'], + database=self.connection_config['dbname'], + warehouse=self.connection_config['warehouse'], + # Use insecure mode to avoid "Failed to get OCSP response" warnings + # + # Further info: https://snowflakecommunity.force.com/s/question/0D50Z00008AEhWbSAL/python-snowflake-connector-ocsp-response-warning-message + # Snowflake is changing certificate authority + insecure_mode=True + ) + + def query(self, query, params=None): + logger.info("SNOWFLAKE - Running query: {}".format(query)) + with self.open_connection() as connection: + with connection.cursor(snowflake.connector.DictCursor) as cur: + cur.execute( + query, + params + ) + + if cur.rowcount > 0: + return cur.fetchall() + + return [] + + def table_name(self, stream_name, is_temporary, without_schema = False): + stream_dict = stream_name_to_dict(stream_name) + table_name = stream_dict['table_name'] + sf_table_name = table_name.replace('.', '_').replace('-', '_').lower() + + if is_temporary: + sf_table_name = '{}_temp'.format(sf_table_name) + + if without_schema: + return '{}'.format(sf_table_name) + + return '{}.{}'.format(self.schema_name, sf_table_name) + + def record_primary_key_string(self, record): + if len(self.stream_schema_message['key_properties']) == 0: + return None + flatten = flatten_record(record) + try: + key_props = [str(flatten[p]) for p in self.stream_schema_message['key_properties']] + except Exception as exc: + logger.info("Cannot find {} primary key(s) in record: {}".format(self.stream_schema_message['key_properties'], flatten)) + raise exc + return ','.join(key_props) + + def record_to_csv_line(self, record): + flatten = flatten_record(record) + return ','.join( + [ + json.dumps(flatten[name], ensure_ascii=False) if name in flatten and (flatten[name] == 0 or flatten[name]) else '' + for name in self.flatten_schema + ] + ) + + def put_to_stage(self, file, stream, count): + logger.info("Uploading {} rows to external snowflake stage on S3".format(count)) + + # Generating key in S3 bucket + bucket = self.connection_config['s3_bucket'] + s3_key_prefix = self.connection_config.get('s3_key_prefix', '') + s3_key = "{}pipelinewise_{}_{}.csv".format(s3_key_prefix, stream, datetime.datetime.now().strftime("%Y%m%d-%H%M%S-%f")) + + logger.info("Target S3 bucket: {}, local file: {}, S3 key: {}".format(bucket, file, s3_key)) + + # Encrypt csv if client side encryption enabled + master_key = self.connection_config.get('client_side_encryption_master_key', '') + if master_key != '': + # Encrypt the file + encryption_material = SnowflakeFileEncryptionMaterial( + query_stage_master_key=master_key, + query_id='', + smk_id=0 + ) + encryption_metadata, encrypted_file = SnowflakeEncryptionUtil.encrypt_file( + encryption_material, + file + ) + + # Upload to s3 + # Send key and iv in the metadata, that will be required to decrypt and upload the encrypted file + metadata = { + 'x-amz-key': encryption_metadata.key, + 'x-amz-iv': encryption_metadata.iv + } + self.s3.upload_file(encrypted_file, bucket, s3_key, ExtraArgs={'Metadata': metadata}) + + # Remove the uploaded encrypted file + os.remove(encrypted_file) + + # Upload to S3 without encrypting + else: + self.s3.upload_file(file, bucket, s3_key) + + return s3_key + + + def delete_from_stage(self, s3_key): + logger.info("Deleting {} from external snowflake stage on S3".format(s3_key)) + bucket = self.connection_config['s3_bucket'] + self.s3.delete_object(Bucket=bucket, Key=s3_key) + + + def load_csv(self, s3_key, count): + stream_schema_message = self.stream_schema_message + stream = stream_schema_message['stream'] + logger.info("Loading {} rows into '{}'".format(count, self.table_name(stream, False))) + + # Get list if columns with types + columns_with_trans = [ + { + "name": safe_column_name(name), + "trans": column_trans(schema) + } + for (name, schema) in self.flatten_schema.items() + ] + + with self.open_connection() as connection: + with connection.cursor(snowflake.connector.DictCursor) as cur: + + # Insert or Update with MERGE command if primary key defined + if len(self.stream_schema_message['key_properties']) > 0: + merge_sql = """MERGE INTO {} t + USING ( + SELECT {} + FROM @{}/{} + (FILE_FORMAT => '{}')) s + ON {} + WHEN MATCHED THEN + UPDATE SET {} + WHEN NOT MATCHED THEN + INSERT ({}) + VALUES ({}) + """.format( + self.table_name(stream, False), + ', '.join(["{}(${}) {}".format(c['trans'], i + 1, c['name']) for i, c in enumerate(columns_with_trans)]), + self.connection_config['stage'], + s3_key, + self.connection_config['file_format'], + self.primary_key_merge_condition(), + ', '.join(['{}=s.{}'.format(c['name'], c['name']) for c in columns_with_trans]), + ', '.join([c['name'] for c in columns_with_trans]), + ', '.join(['s.{}'.format(c['name']) for c in columns_with_trans]) + ) + logger.info("SNOWFLAKE - {}".format(merge_sql)) + cur.execute(merge_sql) + + # Insert only with COPY command if no primary key + else: + copy_sql = """COPY INTO {} ({}) FROM @{}/{} + FILE_FORMAT = (format_name='{}') + """.format( + self.table_name(stream, False), + ', '.join([c['name'] for c in columns_with_trans]), + self.connection_config['stage'], + s3_key, + self.connection_config['file_format'], + ) + logger.info("SNOWFLAKE - {}".format(copy_sql)) + cur.execute(copy_sql) + + logger.info("SNOWFLAKE - Merge into {}: {}".format(self.table_name(stream, False), cur.fetchall())) + + def primary_key_merge_condition(self): + stream_schema_message = self.stream_schema_message + names = primary_column_names(stream_schema_message) + return ' AND '.join(['s.{} = t.{}'.format(c, c) for c in names]) + + def column_names(self): + return [safe_column_name(name) for name in self.flatten_schema] + + def create_table_query(self, is_temporary=False): + stream_schema_message = self.stream_schema_message + columns = [ + column_clause( + name, + schema + ) + for (name, schema) in self.flatten_schema.items() + ] + + primary_key = ["PRIMARY KEY ({})".format(', '.join(primary_column_names(stream_schema_message)))] \ + if len(stream_schema_message['key_properties']) else [] + + return 'CREATE {}TABLE IF NOT EXISTS {} ({}) {}'.format( + 'TEMP ' if is_temporary else '', + self.table_name(stream_schema_message['stream'], is_temporary), + ', '.join(columns + primary_key), + 'data_retention_time_in_days = 0 ' if is_temporary else 'data_retention_time_in_days = 1 ' + ) + + def grant_usage_on_schema(self, schema_name, grantee): + query = "GRANT USAGE ON SCHEMA {} TO ROLE {}".format(schema_name, grantee) + logger.info("Granting USAGE privilegue on '{}' schema to '{}'... {}".format(schema_name, grantee, query)) + self.query(query) + + def grant_select_on_all_tables_in_schema(self, schema_name, grantee): + query = "GRANT SELECT ON ALL TABLES IN SCHEMA {} TO ROLE {}".format(schema_name, grantee) + logger.info("Granting SELECT ON ALL TABLES privilegue on '{}' schema to '{}'... {}".format(schema_name, grantee, query)) + self.query(query) + + @classmethod + def grant_privilege(self, schema, grantees, grant_method): + if isinstance(grantees, list): + for grantee in grantees: + grant_method(schema, grantee) + elif isinstance(grantees, str): + grant_method(schema, grantees) + + def delete_rows(self, stream): + table = self.table_name(stream, False) + query = "DELETE FROM {} WHERE _sdc_deleted_at IS NOT NULL".format(table) + logger.info("Deleting rows from '{}' table... {}".format(table, query)) + logger.info("DELETE {}".format(len(self.query(query)))) + + def create_schema_if_not_exists(self, table_columns_cache=None): + schema_name = self.schema_name + schema_rows = 0 + + # table_columns_cache is an optional pre-collected list of available objects in snowflake + if table_columns_cache: + schema_rows = list(filter(lambda x: x['TABLE_SCHEMA'] == schema_name, table_columns_cache)) + # Query realtime if not pre-collected + else: + schema_rows = self.query( + 'SELECT LOWER(schema_name) schema_name FROM information_schema.schemata WHERE LOWER(schema_name) = %s', + (schema_name.lower(),) + ) + + if len(schema_rows) == 0: + query = "CREATE SCHEMA IF NOT EXISTS {}".format(schema_name) + logger.info("Schema '{}' does not exist. Creating... {}".format(schema_name, query)) + self.query(query) + + self.grant_privilege(schema_name, self.grantees, self.grant_usage_on_schema) + + def get_tables(self, table_schema=None): + return self.query("""SELECT LOWER(table_schema) table_schema, LOWER(table_name) table_name + FROM information_schema.tables + WHERE LOWER(table_schema) = {}""".format( + "LOWER(table_schema)" if table_schema is None else "'{}'".format(table_schema.lower()) + )) + + def get_table_columns(self, table_schema=None, table_name=None, filter_schemas=None): + return self.query("""SELECT LOWER(t.table_schema) table_schema, LOWER(t.table_name) table_name, c.column_name, c.data_type + FROM information_schema.tables t, + information_schema.columns c + WHERE t.table_type = 'BASE TABLE' + AND LOWER(c.table_schema) = {} AND LOWER(c.table_name) = {} + {}""".format( + "LOWER(t.table_schema)" if table_schema is None else "'{}'".format(table_schema.lower()), + "LOWER(t.table_name)" if table_name is None else "'{}'".format(table_name.lower()), + "" if not filter_schemas else " AND LOWER(c.table_schema) IN ({})".format(', '.join("'{0}'".format(s) for s in filter_schemas)) + )) + + def update_columns(self, table_columns_cache=None): + stream_schema_message = self.stream_schema_message + stream = stream_schema_message['stream'] + table_name = self.table_name(stream, False, True) + schema_name = self.schema_name + columns = [] + if table_columns_cache: + columns = list(filter(lambda x: x['TABLE_SCHEMA'] == self.schema_name.lower() and x['TABLE_NAME'].lower() == table_name, table_columns_cache)) + else: + columns = self.get_table_columns(schema_name, table_name) + columns_dict = {column['COLUMN_NAME'].lower(): column for column in columns} + + columns_to_add = [ + column_clause( + name, + properties_schema + ) + for (name, properties_schema) in self.flatten_schema.items() + if name.lower() not in columns_dict + ] + + for column in columns_to_add: + self.add_column(column, stream) + + columns_to_replace = [ + (safe_column_name(name), column_clause( + name, + properties_schema + )) + for (name, properties_schema) in self.flatten_schema.items() + if name.lower() in columns_dict and + columns_dict[name.lower()]['DATA_TYPE'].lower() != column_type(properties_schema).lower() and + + # Don't alter table if TIMESTAMP_NTZ detected as the new required column type + # + # Target-snowflake maps every data-time JSON types to TIMESTAMP_NTZ but sometimes + # a TIMESTAMP_TZ column is alrady available in the target table (i.e. created by fastsync initial load) + # We need to exclude this conversion otherwise we loose the data that is already populated + # in the column + # + # TODO: Support both TIMESTAMP_TZ and TIMESTAMP_NTZ in target-snowflake + # when extracting data-time values from JSON + # (Check the column_type function for further details) + column_type(properties_schema).lower() != 'timestamp_ntz' + ] + + for (column_name, column) in columns_to_replace: + self.drop_column(column_name, stream) + self.add_column(column, stream) + + def add_column(self, column, stream): + add_column = "ALTER TABLE {} ADD COLUMN {}".format(self.table_name(stream, False), column) + logger.info('Adding column: {}'.format(add_column)) + self.query(add_column) + + def drop_column(self, column_name, stream): + drop_column = "ALTER TABLE {} DROP COLUMN {}".format(self.table_name(stream, False), column_name) + logger.info('Dropping column: {}'.format(drop_column)) + self.query(drop_column) + + def sync_table(self, table_columns_cache=None): + stream_schema_message = self.stream_schema_message + stream = stream_schema_message['stream'] + table_name = self.table_name(stream, False, True) + table_name_with_schema = self.table_name(stream, False) + found_tables = [] + + if table_columns_cache: + found_tables = list(filter(lambda x: x['TABLE_SCHEMA'] == self.schema_name.lower() and x['TABLE_NAME'].lower() == table_name, table_columns_cache)) + else: + found_tables = [table for table in (self.get_tables(self.schema_name.lower())) if table['TABLE_NAME'].lower() == table_name] + + if len(found_tables) == 0: + query = self.create_table_query() + logger.info("Table '{}' does not exist. Creating...".format(table_name_with_schema)) + self.query(query) + + self.grant_privilege(self.schema_name, self.grantees, self.grant_select_on_all_tables_in_schema) + else: + logger.info("Table '{}' exists".format(table_name_with_schema)) + self.update_columns(table_columns_cache) + diff --git a/tests/integration/resources/invalid-json.json b/tests/integration/resources/invalid-json.json new file mode 100644 index 0000000..13784c2 --- /dev/null +++ b/tests/integration/resources/invalid-json.json @@ -0,0 +1,4 @@ +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_one"}} +{"type": "SCHEMA", "stream": "tap_mysql_test-test_table_one", "schema": {"properties": {"c_pk": {"inclusion": "automatic", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_varchar": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}, "c_int": {"inclusion": "available", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}}, "type": "object"}, "key_properties": ["c_pk"]} +THIS IS A TEST INPUT FROM A TAP WITH A LINE WITH INVALID JSON +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_one", "version": 1} diff --git a/tests/integration/resources/invalid-message-order.json b/tests/integration/resources/invalid-message-order.json new file mode 100644 index 0000000..8e8cc37 --- /dev/null +++ b/tests/integration/resources/invalid-message-order.json @@ -0,0 +1,3 @@ +{"type": "SCHEMA", "stream": "tap_mysql_test-test_table_one", "schema": {"properties": {"c_pk": {"inclusion": "automatic", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_varchar": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}, "c_int": {"inclusion": "available", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}}, "type": "object"}, "key_properties": ["c_pk"]} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_one", "record": {"c_pk": 1, "c_varchar": "1", "c_int": 1}, "version": 1, "time_extracted": "2019-01-31T15:51:47.465408Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_two", "record": {"c_pk": 2, "c_varchar": "2", "c_int": 2, "c_date": "2019-02-10 02:00:00"}, "version": 3, "time_extracted": "2019-01-31T15:51:48.861962Z"} \ No newline at end of file diff --git a/tests/integration/resources/messages-with-multi-schemas.json b/tests/integration/resources/messages-with-multi-schemas.json new file mode 100644 index 0000000..993e258 --- /dev/null +++ b/tests/integration/resources/messages-with-multi-schemas.json @@ -0,0 +1,26 @@ +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_one"}} +{"type": "SCHEMA", "stream": "tap_mysql_test-test_table_one", "schema": {"properties": {"c_pk": {"inclusion": "automatic", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_varchar": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}, "c_int": {"inclusion": "available", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}}, "type": "object"}, "key_properties": ["c_pk"]} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_one", "version": 1} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_one", "record": {"c_pk": 1, "c_varchar": "1", "c_int": 1}, "version": 1, "time_extracted": "2019-01-31T15:51:47.465408Z"} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_one"}} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_one", "version": 1} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_one", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}}}} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_two", "bookmarks": {"tap_mysql_test-test_table_two": {"initial_full_table_complete": true}}}} +{"type": "SCHEMA", "stream": "tap_mysql_test-test_table_two", "schema": {"properties": {"c_pk": {"inclusion": "automatic", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_varchar": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}, "c_int": {"inclusion": "available", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_date": {"format": "date-time", "inclusion": "available", "type": ["null", "string"]}}, "type": "object"}, "key_properties": ["c_pk"]} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_two", "version": 3} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_two", "record": {"c_pk": 1, "c_varchar": "1", "c_int": 1, "c_date": "2019-02-01 15:12:45", "_sdc_deleted_at": "2019-02-12T01:10:10.000000Z"}, "version": 3, "time_extracted": "2019-01-31T15:51:48.861962Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_two", "record": {"c_pk": 2, "c_varchar": "2", "c_int": 2, "c_date": "2019-02-10 02:00:00"}, "version": 3, "time_extracted": "2019-01-31T15:51:48.861962Z"} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_two", "bookmarks": {"tap_mysql_test-test_table_wo": {"initial_full_table_complete": true}}}} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_three", "version": 3} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_two", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_three": {"initial_full_table_complete": true}}}} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_three", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_three": {"initial_full_table_complete": true}}}} +{"type": "SCHEMA", "stream": "tap_mysql_test-test_table_three", "schema": {"properties": {"c_pk": {"inclusion": "automatic", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_varchar": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}, "c_int": {"inclusion": "available", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_time": {"format": "time", "inclusion": "available", "type": ["null", "string"]}}, "type": "object"}, "key_properties": ["c_pk"]} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_three", "version": 2} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_three", "record": {"c_pk": 1, "c_varchar": "1", "c_int": 1, "c_time": "04:00:00"}, "version": 2, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_three", "record": {"c_pk": 2, "c_varchar": "2", "c_int": 2, "c_time": "07:15:00"}, "version": 2, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "SCHEMA", "stream": "tap_mysql_test-test_table_three", "schema": {"properties": {"c_pk": {"inclusion": "automatic", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_varchar": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}, "c_int": {"inclusion": "available", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_time": {"format": "time", "inclusion": "available", "type": ["null", "string"]}}, "type": "object"}, "key_properties": ["c_pk"]} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_three", "record": {"c_pk": 3, "c_varchar": "3", "c_int": 3, "c_time": "23:00:03", "_sdc_deleted_at": "2019-02-10T15:51:50.215998Z"}, "version": 2, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_three", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_two": {"initial_full_table_complete": true}}}} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_three", "version": 2} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_three", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_two": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_three": {"initial_full_table_complete": true}}}} +{"type": "STATE", "value": {"currently_syncing": null, "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_two": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_three": {"initial_full_table_complete": true}}}} diff --git a/tests/integration/resources/messages-with-non-db-friendly-columns.json b/tests/integration/resources/messages-with-non-db-friendly-columns.json new file mode 100644 index 0000000..f796687 --- /dev/null +++ b/tests/integration/resources/messages-with-non-db-friendly-columns.json @@ -0,0 +1,11 @@ +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_non_db_friendly_columns"}} +{"type": "SCHEMA", "stream": "tap_mysql_test-test_table_non_db_friendly_columns", "schema": {"properties": {"c_pk": {"inclusion": "automatic", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "camelcaseColumn": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}, "minus-column": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}}, "type": "object"}, "key_properties": ["c_pk"]} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_non_db_friendly_columns", "version": 1} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_non_db_friendly_columns", "record": {"c_pk": 1, "camelcaseColumn": "Dummy row 1", "minus-column": "Dummy row 1"}, "version": 1, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_non_db_friendly_columns", "record": {"c_pk": 2, "camelcaseColumn": "Dummy row 2", "minus-column": "Dummy row 2"}, "version": 1, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_non_db_friendly_columns", "record": {"c_pk": 3, "camelcaseColumn": "Dummy row 3", "minus-column": "Dummy row 3"}, "version": 1, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_non_db_friendly_columns", "record": {"c_pk": 4, "camelcaseColumn": "Dummy row 4", "minus-column": "Dummy row 4"}, "version": 1, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_non_db_friendly_columns", "record": {"c_pk": 5, "camelcaseColumn": "Dummy row 5", "minus-column": "Dummy row 5"}, "version": 1, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_non_db_friendly_columns", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_two": {"initial_full_table_complete": true}}}} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_non_db_friendly_columns", "version": 1} +{"type": "STATE", "value": {"currently_syncing": null, "bookmarks": {"tap_mysql_test-test_table_non_db_friendly_columns": {"initial_full_table_complete": true}}}} diff --git a/tests/integration/resources/messages-with-three-streams.json b/tests/integration/resources/messages-with-three-streams.json new file mode 100644 index 0000000..975cc0f --- /dev/null +++ b/tests/integration/resources/messages-with-three-streams.json @@ -0,0 +1,25 @@ +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_one"}} +{"type": "SCHEMA", "stream": "tap_mysql_test-test_table_one", "schema": {"properties": {"c_pk": {"inclusion": "automatic", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_varchar": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}, "c_int": {"inclusion": "available", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}}, "type": "object"}, "key_properties": ["c_pk"]} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_one", "version": 1} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_one", "record": {"c_pk": 1, "c_varchar": "1", "c_int": 1}, "version": 1, "time_extracted": "2019-01-31T15:51:47.465408Z"} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_one"}} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_one", "version": 1} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_one", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}}}} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_two", "bookmarks": {"tap_mysql_test-test_table_two": {"initial_full_table_complete": true}}}} +{"type": "SCHEMA", "stream": "tap_mysql_test-test_table_two", "schema": {"properties": {"c_pk": {"inclusion": "automatic", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_varchar": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}, "c_int": {"inclusion": "available", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_date": {"format": "date-time", "inclusion": "available", "type": ["null", "string"]}}, "type": "object"}, "key_properties": ["c_pk"]} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_two", "version": 3} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_two", "record": {"c_pk": 1, "c_varchar": "1", "c_int": 1, "c_date": "2019-02-01 15:12:45", "_sdc_deleted_at": "2019-02-12T01:10:10.000000Z"}, "version": 3, "time_extracted": "2019-01-31T15:51:48.861962Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_two", "record": {"c_pk": 2, "c_varchar": "2", "c_int": 2, "c_date": "2019-02-10 02:00:00"}, "version": 3, "time_extracted": "2019-01-31T15:51:48.861962Z"} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_two", "bookmarks": {"tap_mysql_test-test_table_wo": {"initial_full_table_complete": true}}}} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_three", "version": 3} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_two", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_three": {"initial_full_table_complete": true}}}} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_three", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_three": {"initial_full_table_complete": true}}}} +{"type": "SCHEMA", "stream": "tap_mysql_test-test_table_three", "schema": {"properties": {"c_pk": {"inclusion": "automatic", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_varchar": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}, "c_int": {"inclusion": "available", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_time": {"format": "time", "inclusion": "available", "type": ["null", "string"]}}, "type": "object"}, "key_properties": ["c_pk"]} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_three", "version": 2} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_three", "record": {"c_pk": 1, "c_varchar": "1", "c_int": 1, "c_time": "04:00:00"}, "version": 2, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_three", "record": {"c_pk": 2, "c_varchar": "2", "c_int": 2, "c_time": "07:15:00"}, "version": 2, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_three", "record": {"c_pk": 3, "c_varchar": "3", "c_int": 3, "c_time": "23:00:03", "_sdc_deleted_at": "2019-02-10T15:51:50.215998Z"}, "version": 2, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_three", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_two": {"initial_full_table_complete": true}}}} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_three", "version": 2} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_three", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_two": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_three": {"initial_full_table_complete": true}}}} +{"type": "STATE", "value": {"currently_syncing": null, "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_two": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_three": {"initial_full_table_complete": true}}}} diff --git a/tests/integration/resources/messages-with-unicode-characters.json b/tests/integration/resources/messages-with-unicode-characters.json new file mode 100644 index 0000000..22303aa --- /dev/null +++ b/tests/integration/resources/messages-with-unicode-characters.json @@ -0,0 +1,12 @@ +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_unicode"}} +{"type": "SCHEMA", "stream": "tap_mysql_test-test_table_unicode", "schema": {"properties": {"c_pk": {"inclusion": "automatic", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}, "c_varchar": {"inclusion": "available", "maxLength": 16, "type": ["null", "string"]}, "c_int": {"inclusion": "available", "minimum": -2147483648, "maximum": 2147483647, "type": ["null", "integer"]}}, "type": "object"}, "key_properties": ["c_pk"]} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_unicode", "version": 1} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_unicode", "record": {"c_pk": 1, "c_varchar": "Hello world, \u039a\u03b1\u03bb\u03b7\u03bc\u1f73\u03c1\u03b1 \u03ba\u1f79\u03c3\u03bc\u03b5, \u30b3\u30f3\u30cb\u30c1\u30cf", "c_int": 1}, "version": 1, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_unicode", "record": {"c_pk": 2, "c_varchar": "Chinese: \u548c\u6bdb\u6cfd\u4e1c <<\u91cd\u4e0a\u4e95\u5188\u5c71>>. \u4e25\u6c38\u6b23, \u4e00\u4e5d\u516b\u516b\u5e74.", "c_int": 2}, "version": 1, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_unicode", "record": {"c_pk": 3, "c_varchar": "Russian: \u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0439\u0442\u0435\u0441\u044c \u0441\u0435\u0439\u0447\u0430\u0441 \u043d\u0430 \u0414\u0435\u0441\u044f\u0442\u0443\u044e \u041c\u0435\u0436\u0434\u0443\u043d\u0430\u0440\u043e\u0434\u043d\u0443\u044e \u041a\u043e\u043d\u0444\u0435\u0440\u0435\u043d\u0446\u0438\u044e \u043f\u043e", "c_int": 3}, "version": 1, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_unicode", "record": {"c_pk": 4, "c_varchar": "Thai: \u0e41\u0e1c\u0e48\u0e19\u0e14\u0e34\u0e19\u0e2e\u0e31\u0e48\u0e19\u0e40\u0e2a\u0e37\u0e48\u0e2d\u0e21\u0e42\u0e17\u0e23\u0e21\u0e41\u0e2a\u0e19\u0e2a\u0e31\u0e07\u0e40\u0e27\u0e0a", "c_int": 4, "_sdc_deleted_at": "2019-02-10T15:51:50.215998Z"}, "version": 1, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_unicode", "record": {"c_pk": 5, "c_varchar": "Arabic: \u0644\u0642\u062f \u0644\u0639\u0628\u062a \u0623\u0646\u062a \u0648\u0623\u0635\u062f\u0642\u0627\u0624\u0643 \u0644\u0645\u062f\u0629 \u0648\u062d\u0635\u0644\u062a\u0645 \u0639\u0644\u064a \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0646\u0642\u0627\u0637", "c_int": 5, "_sdc_deleted_at": "2019-02-10T15:51:50.215998Z"}, "version": 1, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "RECORD", "stream": "tap_mysql_test-test_table_unicode", "record": {"c_pk": 6, "c_varchar": "Special Characters: [\",'!@£$%^&*()]", "c_int": 6}, "version": 1, "time_extracted": "2019-01-31T15:51:50.215998Z"} +{"type": "STATE", "value": {"currently_syncing": "tap_mysql_test-test_table_unicode", "bookmarks": {"tap_mysql_test-test_table_one": {"initial_full_table_complete": true}, "tap_mysql_test-test_table_two": {"initial_full_table_complete": true}}}} +{"type": "ACTIVATE_VERSION", "stream": "tap_mysql_test-test_table_unicode", "version": 1} +{"type": "STATE", "value": {"currently_syncing": null, "bookmarks": {"tap_mysql_test-test_table_unicode": {"initial_full_table_complete": true}}}} diff --git a/tests/integration/test_target_snowflake.py b/tests/integration/test_target_snowflake.py new file mode 100644 index 0000000..00d2cf9 --- /dev/null +++ b/tests/integration/test_target_snowflake.py @@ -0,0 +1,286 @@ +import unittest +import os +import json +import datetime +import target_snowflake +import snowflake + +from nose.tools import assert_raises +from target_snowflake.db_sync import DbSync +from snowflake.connector.encryption_util import SnowflakeEncryptionUtil +from snowflake.connector.remote_storage_util import SnowflakeFileEncryptionMaterial + +try: + import tests.utils as test_utils +except ImportError: + import utils as test_utils + + +METADATA_COLUMNS = [ + '_SDC_BATCHED_AT', + '_SDC_DELETED_AT', + '_SDC_EXTRACTED_AT', + '_SDC_PRIMARY_KEY', + '_SDC_RECEIVED_AT', + '_SDC_SEQUENCE', + '_SDC_TABLE_VERSION' +] + + +class TestIntegration(unittest.TestCase): + """ + Integration Tests + """ + @classmethod + def setUp(self): + self.config = test_utils.get_test_config() + snowflake = DbSync(self.config) + if self.config['default_target_schema']: + snowflake.query("DROP SCHEMA IF EXISTS {}".format(self.config['default_target_schema'])) + + + def remove_metadata_columns_from_rows(self, rows): + """Removes metadata columns from a list of rows""" + d_rows = [] + for r in rows: + # Copy the original row to a new dict to keep the original dict + # and remove metadata columns + d_row = r.copy() + for md_c in METADATA_COLUMNS: + d_row.pop(md_c, None) + + # Add new row without metadata columns to the new list + d_rows.append(d_row) + + return d_rows + + + def assert_metadata_columns_exist(self, rows): + """This is a helper assertion that checks if every row in a list has metadata columns""" + for r in rows: + for md_c in METADATA_COLUMNS: + self.assertTrue(md_c in r) + + + def assert_metadata_columns_not_exist(self, rows): + """This is a helper assertion that checks metadata columns don't exist in any row""" + for r in rows: + for md_c in METADATA_COLUMNS: + self.assertFalse(md_c in r) + + + def assert_three_streams_are_into_snowflake(self, should_metadata_columns_exist=False, should_hard_deleted_rows=False): + """ + This is a helper assertion that checks if every data from the message-with-three-streams.json + file is available in Snowflake tables correctly. + Useful to check different loading methods (unencrypted, Client-Side encryption, gzip, etc.) + without duplicating assertions + """ + snowflake = DbSync(self.config) + default_target_schema = self.config.get('default_target_schema', '') + schema_mapping = self.config.get('schema_mapping', {}) + + # Identify target schema name + target_schema = None + if default_target_schema is not None and default_target_schema.strip(): + target_schema = default_target_schema + elif schema_mapping: + target_schema = "tap_mysql_test" + + # Get loaded rows from tables + table_one = snowflake.query("SELECT * FROM {}.test_table_one ORDER BY c_pk".format(target_schema)) + table_two = snowflake.query("SELECT * FROM {}.test_table_two ORDER BY c_pk".format(target_schema)) + table_three = snowflake.query("SELECT * FROM {}.test_table_three ORDER BY c_pk".format(target_schema)) + + + # ---------------------------------------------------------------------- + # Check rows in table_one + # ---------------------------------------------------------------------- + expected_table_one = [ + {'C_INT': 1, 'C_PK': 1, 'C_VARCHAR': '1'} + ] + + self.assertEqual( + self.remove_metadata_columns_from_rows(table_one), expected_table_one) + + # ---------------------------------------------------------------------- + # Check rows in table_tow + # ---------------------------------------------------------------------- + expected_table_two = [] + if not should_hard_deleted_rows: + expected_table_two = [ + {'C_INT': 1, 'C_PK': 1, 'C_VARCHAR': '1', 'C_DATE': datetime.datetime(2019, 2, 1, 15, 12, 45)}, + {'C_INT': 2, 'C_PK': 2, 'C_VARCHAR': '2', 'C_DATE': datetime.datetime(2019, 2, 10, 2, 0, 0)} + ] + else: + expected_table_two = [ + {'C_INT': 2, 'C_PK': 2, 'C_VARCHAR': '2', 'C_DATE': datetime.datetime(2019, 2, 10, 2, 0, 0)} + ] + + self.assertEqual( + self.remove_metadata_columns_from_rows(table_two), expected_table_two) + + # ---------------------------------------------------------------------- + # Check rows in table_three + # ---------------------------------------------------------------------- + expected_table_three = [] + if not should_hard_deleted_rows: + expected_table_three = [ + {'C_INT': 1, 'C_PK': 1, 'C_VARCHAR': '1', 'C_TIME': datetime.time(4, 0, 0)}, + {'C_INT': 2, 'C_PK': 2, 'C_VARCHAR': '2', 'C_TIME': datetime.time(7, 15, 0)}, + {'C_INT': 3, 'C_PK': 3, 'C_VARCHAR': '3', 'C_TIME': datetime.time(23, 0, 3)} + ] + else: + expected_table_three = [ + {'C_INT': 1, 'C_PK': 1, 'C_VARCHAR': '1', 'C_TIME': datetime.time(4, 0, 0)}, + {'C_INT': 2, 'C_PK': 2, 'C_VARCHAR': '2', 'C_TIME': datetime.time(7, 15, 0)} + ] + + self.assertEqual( + self.remove_metadata_columns_from_rows(table_three), expected_table_three) + + # ---------------------------------------------------------------------- + # Check if metadata columns exist or not + # ---------------------------------------------------------------------- + if should_metadata_columns_exist: + self.assert_metadata_columns_exist(table_one) + self.assert_metadata_columns_exist(table_two) + self.assert_metadata_columns_exist(table_three) + else: + self.assert_metadata_columns_not_exist(table_one) + self.assert_metadata_columns_not_exist(table_two) + self.assert_metadata_columns_not_exist(table_three) + + + def test_invalid_json(self): + """Receiving invalid JSONs should raise an exception""" + tap_lines = test_utils.get_test_tap_lines('invalid-json.json') + with assert_raises(json.decoder.JSONDecodeError): + target_snowflake.persist_lines(self.config, tap_lines) + + + def test_message_order(self): + """RECORD message without a previously received SCHEMA message should raise an exception""" + tap_lines = test_utils.get_test_tap_lines('invalid-message-order.json') + with assert_raises(Exception): + target_snowflake.persist_lines(self.config, tap_lines) + + + def test_loading_tables_with_no_encryption(self): + """Loading multiple tables from the same input tap with various columns types""" + tap_lines = test_utils.get_test_tap_lines('messages-with-three-streams.json') + + # Turning off client-side encryption and load + self.config['client_side_encryption_master_key'] = '' + target_snowflake.persist_lines(self.config, tap_lines) + + self.assert_three_streams_are_into_snowflake() + + + def test_loading_tables_with_client_side_encryption(self): + """Loading multiple tables from the same input tap with various columns types""" + tap_lines = test_utils.get_test_tap_lines('messages-with-three-streams.json') + + # Turning on client-side encryption and load + self.config['client_side_encryption_master_key'] = os.environ.get('CLIENT_SIDE_ENCRYPTION_MASTER_KEY') + target_snowflake.persist_lines(self.config, tap_lines) + + self.assert_three_streams_are_into_snowflake() + + + def test_loading_tables_with_client_side_encryption_and_wrong_master_key(self): + """Loading multiple tables from the same input tap with various columns types""" + tap_lines = test_utils.get_test_tap_lines('messages-with-three-streams.json') + + # Turning on client-side encryption and load but using a well formatted but wrong master key + self.config['client_side_encryption_master_key'] = "Wr0n6m45t3rKeY0123456789a0123456789a0123456=" + with assert_raises(snowflake.connector.errors.ProgrammingError): + target_snowflake.persist_lines(self.config, tap_lines) + + + def test_loading_tables_with_metadata_columns(self): + """Loading multiple tables from the same input tap with various columns types""" + tap_lines = test_utils.get_test_tap_lines('messages-with-three-streams.json') + + # Turning on adding metadata columns + self.config['add_metadata_columns'] = True + target_snowflake.persist_lines(self.config, tap_lines) + + # Check if data loaded correctly and metadata columns exist + self.assert_three_streams_are_into_snowflake(should_metadata_columns_exist=True) + + + def test_loading_tables_with_hard_delete(self): + """Loading multiple tables from the same input tap with deleted rows""" + tap_lines = test_utils.get_test_tap_lines('messages-with-three-streams.json') + + # Turning on hard delete mode + self.config['hard_delete'] = True + target_snowflake.persist_lines(self.config, tap_lines) + + # Check if data loaded correctly and metadata columns exist + self.assert_three_streams_are_into_snowflake( + should_metadata_columns_exist=True, + should_hard_deleted_rows=True + ) + + + def test_loading_with_multiple_schema(self): + """Loading table with multiple SCHEMA messages""" + tap_lines = test_utils.get_test_tap_lines('messages-with-multi-schemas.json') + + # Load with default settings + target_snowflake.persist_lines(self.config, tap_lines) + + # Check if data loaded correctly + self.assert_three_streams_are_into_snowflake( + should_metadata_columns_exist=False, + should_hard_deleted_rows=False + ) + + + def test_loading_unicode_characters(self): + """Loading unicode encoded characters""" + tap_lines = test_utils.get_test_tap_lines('messages-with-unicode-characters.json') + + # Load with default settings + target_snowflake.persist_lines(self.config, tap_lines) + + # Get loaded rows from tables + snowflake = DbSync(self.config) + target_schema = self.config.get('default_target_schema', '') + table_unicode = snowflake.query("SELECT * FROM {}.test_table_unicode".format(target_schema)) + + self.assertEqual( + table_unicode, + [ + {'C_INT': 1, 'C_PK': 1, 'C_VARCHAR': 'Hello world, Καλημέρα κόσμε, コンニチハ'}, + {'C_INT': 2, 'C_PK': 2, 'C_VARCHAR': 'Chinese: 和毛泽东 <<重上井冈山>>. 严永欣, 一九八八年.'}, + {'C_INT': 3, 'C_PK': 3, 'C_VARCHAR': 'Russian: Зарегистрируйтесь сейчас на Десятую Международную Конференцию по'}, + {'C_INT': 4, 'C_PK': 4, 'C_VARCHAR': 'Thai: แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช'}, + {'C_INT': 5, 'C_PK': 5, 'C_VARCHAR': 'Arabic: لقد لعبت أنت وأصدقاؤك لمدة وحصلتم علي من إجمالي النقاط'}, + {'C_INT': 6, 'C_PK': 6, 'C_VARCHAR': 'Special Characters: [",\'!@£$%^&*()]'} + ]) + + + def test_non_db_friendly_columns(self): + """Loading non-db friendly columns like, camelcase, minus signs, etc.""" + tap_lines = test_utils.get_test_tap_lines('messages-with-non-db-friendly-columns.json') + + # Load with default settings + target_snowflake.persist_lines(self.config, tap_lines) + + # Get loaded rows from tables + snowflake = DbSync(self.config) + target_schema = self.config.get('default_target_schema', '') + table_non_db_friendly_columns = snowflake.query("SELECT * FROM {}.test_table_non_db_friendly_columns ORDER BY c_pk".format(target_schema)) + + self.assertEqual( + table_non_db_friendly_columns, + [ + {'C_PK': 1, 'CAMELCASECOLUMN': 'Dummy row 1', 'MINUS-COLUMN': 'Dummy row 1'}, + {'C_PK': 2, 'CAMELCASECOLUMN': 'Dummy row 2', 'MINUS-COLUMN': 'Dummy row 2'}, + {'C_PK': 3, 'CAMELCASECOLUMN': 'Dummy row 3', 'MINUS-COLUMN': 'Dummy row 3'}, + {'C_PK': 4, 'CAMELCASECOLUMN': 'Dummy row 4', 'MINUS-COLUMN': 'Dummy row 4'}, + {'C_PK': 5, 'CAMELCASECOLUMN': 'Dummy row 5', 'MINUS-COLUMN': 'Dummy row 5'}, + ]) diff --git a/tests/integration/utils.py b/tests/integration/utils.py new file mode 100644 index 0000000..f892bda --- /dev/null +++ b/tests/integration/utils.py @@ -0,0 +1,60 @@ +import os +import json + + +def get_db_config(): + config = {} + + # -------------------------------------------------------------------------- + # Default configuration settings for integration tests. + # -------------------------------------------------------------------------- + # The following values needs to be defined in environment variables with + # valid details to a Snowflake instace, AWS IAM role and an S3 bucket + # -------------------------------------------------------------------------- + # Snowflake instance + config['account'] = os.environ.get('TARGET_SNOWFLAKE_ACCOUNT') + config['dbname'] = os.environ.get('TARGET_SNOWFLAKE_DBNAME') + config['user'] = os.environ.get('TARGET_SNOWFLAKE_USER') + config['password'] = os.environ.get('TARGET_SNOWFLAKE_PASSWORD') + config['warehouse'] = os.environ.get('TARGET_SNOWFLAKE_WAREHOUSE') + config['default_target_schema'] = os.environ.get("TARGET_SNOWFLAKE_SCHEMA") + config['stage'] = os.environ.get("TARGET_SNOWFLAKE_STAGE") + config['file_format'] = os.environ.get("TARGET_SNOWFLAKE_FILE_FORMAT") + + # AWS IAM and S3 bucket + config['aws_access_key_id'] = os.environ.get('TARGET_SNOWFLAKE_AWS_ACCESS_KEY') + config['aws_secret_access_key'] = os.environ.get('TARGET_SNOWFLAKE_AWS_SECRET_ACCESS_KEY') + config['s3_bucket'] = os.environ.get('TARGET_SNOWFLAKE_S3_BUCKET') + config['s3_key_prefix'] = os.environ.get('TARGET_SNOWFLAKE_S3_KEY_PREFIX') + + # External stage in snowflake with client side encryption details + config['client_side_encryption_master_key'] = os.environ.get('CLIENT_SIDE_ENCRYPTION_MASTER_KEY') + + + # -------------------------------------------------------------------------- + # The following variables needs to be empty. + # The tests cases will set them automatically whenever it's needed + # -------------------------------------------------------------------------- + config['disable_table_cache'] = None + config['schema_mapping'] = None + config['add_metadata_columns'] = None + config['hard_delete'] = None + + + return config + + +def get_test_config(): + db_config = get_db_config() + + return db_config + + +def get_test_tap_lines(filename): + lines = [] + with open('{}/resources/{}'.format(os.path.dirname(__file__), filename)) as tap_stdout: + for line in tap_stdout.readlines(): + lines.append(line) + + return lines + diff --git a/tests/unit/test_unit.py b/tests/unit/test_unit.py new file mode 100644 index 0000000..2d35fbb --- /dev/null +++ b/tests/unit/test_unit.py @@ -0,0 +1,90 @@ +import unittest +from nose.tools import assert_raises + +import target_snowflake + + +class TestUnit(unittest.TestCase): + """ + Unit Tests + """ + @classmethod + def setUp(self): + self.config = {} + + + def test_config_validation(self): + """Test configuration validator""" + validator = target_snowflake.db_sync.validate_config + empty_config = {} + minimal_config = { + 'account': "dummy-value", + 'dbname': "dummy-value", + 'user': "dummy-value", + 'password': "dummy-value", + 'warehouse': "dummy-value", + 'aws_access_key_id': "dummy-value", + 'aws_secret_access_key': "dummy-value", + 's3_bucket': "dummy-value", + 'default_target_schema': "dummy-value", + 'stage': "dummy-value", + 'file_format': "dummy-value" + } + + # Config validator returns a list of errors + # If the list is empty then the configuration is valid otherwise invalid + + # Empty configuration should fail - (nr_of_errors >= 0) + self.assertGreater(len(validator(empty_config)), 0) + + # Minimal configuratino should pass - (nr_of_errors == 0) + self.assertEqual(len(validator(minimal_config)), 0) + + # Configuration without schema references - (nr_of_errors >= 0) + config_with_no_schema = minimal_config.copy() + config_with_no_schema.pop('default_target_schema') + self.assertGreater(len(validator(config_with_no_schema)), 0) + + # Configuration with schema mapping - (nr_of_errors >= 0) + config_with_schema_mapping = minimal_config.copy() + config_with_schema_mapping.pop('default_target_schema') + config_with_schema_mapping['schema_mapping'] = { + "dummy_stream": { + "target_schema": "dummy_schema" + } + } + self.assertEqual(len(validator(config_with_schema_mapping)), 0) + + + def test_column_type_mapping(self): + """Test JSON type to Snowflake column type mappings""" + mapper = target_snowflake.db_sync.column_type + + # Incoming JSON schema types + json_str = {"type": ["string"] } + json_str_or_null = {"type": ["string", "null"] } + json_dt = {"type": ["string"] , "format": "date-time"} + json_dt_or_null = {"type": ["string", "null"] , "format": "date-time"} + json_t = {"type": ["string"] , "format": "time"} + json_t_or_null = {"type": ["string", "null"] , "format": "time"} + json_num = {"type": ["number"] } + json_int = {"type": ["integer"] } + json_int_or_str = {"type": ["integer", "string"] } + json_bool = {"type": ["boolean"] } + json_obj = {"type": ["object"] } + json_arr = {"type": ["array"] } + + # Mapping from JSON schema types ot Snowflake column types + self.assertEquals(mapper(json_str) , 'text') + self.assertEquals(mapper(json_str_or_null) , 'text') + self.assertEquals(mapper(json_dt) , 'timestamp_ntz') + self.assertEquals(mapper(json_dt_or_null) , 'timestamp_ntz') + self.assertEquals(mapper(json_t) , 'time') + self.assertEquals(mapper(json_t_or_null) , 'time') + self.assertEquals(mapper(json_num) , 'float') + self.assertEquals(mapper(json_int) , 'number') + self.assertEquals(mapper(json_int_or_str) , 'text') + self.assertEquals(mapper(json_bool) , 'boolean') + self.assertEquals(mapper(json_obj) , 'variant') + self.assertEquals(mapper(json_arr) , 'variant') +