From a6ea6d126c7daf892d194784d681b67baa305a32 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Sat, 11 May 2024 22:54:22 +0300 Subject: [PATCH 01/20] docs: Removed deprecated getting-started.md added transformation engines and dynamic parameters pages. Fixed links --- docs/assets/built_in_transformers/img.png | Bin 0 -> 16273 bytes docs/assets/getting_started/list-dumps.png | Bin 8111 -> 0 bytes .../list-transformers-example.png | Bin 60568 -> 0 bytes .../getting_started/validate-result.png | Bin 60751 -> 0 bytes .../advanced_transformers/template_record.md | 2 +- .../dynamic_parameters.md | 136 ++++++ docs/built_in_transformers/index.md | 10 +- .../transformation_engines.md | 1 + docs/getting-started.md | 408 ------------------ mkdocs.yml | 4 +- 10 files changed, 149 insertions(+), 412 deletions(-) create mode 100644 docs/assets/built_in_transformers/img.png delete mode 100644 docs/assets/getting_started/list-dumps.png delete mode 100644 docs/assets/getting_started/list-transformers-example.png delete mode 100644 docs/assets/getting_started/validate-result.png create mode 100644 docs/built_in_transformers/dynamic_parameters.md create mode 100644 docs/built_in_transformers/transformation_engines.md delete mode 100644 docs/getting-started.md diff --git a/docs/assets/built_in_transformers/img.png b/docs/assets/built_in_transformers/img.png new file mode 100644 index 0000000000000000000000000000000000000000..d97982c31d9f4de3dc22d832852ddf3e2be96884 GIT binary patch literal 16273 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 literal 0 HcmV?d00001 diff --git a/docs/assets/getting_started/list-dumps.png b/docs/assets/getting_started/list-dumps.png deleted file mode 100644 index 2f16c565a516ee4097949385d7c4eccf3510e72a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8111 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 diff --git a/docs/assets/getting_started/validate-result.png b/docs/assets/getting_started/validate-result.png deleted file mode 100644 index d41029803f7503d5aaba20a22a8d53ca28325637..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60751 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 diff --git a/docs/built_in_transformers/advanced_transformers/template_record.md b/docs/built_in_transformers/advanced_transformers/template_record.md index 716f0a66..57326ecf 100644 --- a/docs/built_in_transformers/advanced_transformers/template_record.md +++ b/docs/built_in_transformers/advanced_transformers/template_record.md @@ -10,7 +10,7 @@ Modify records using a Go template and apply changes by using the PostgreSQL dri ## Description -`TemplateRecord` uses [Go templates](https://pkg.go.dev/text/template) to change data. However, while the [Template transformer](/template.md) operates with a single column and automatically applies results, the `TemplateRecord` transformer can make changes to a set of columns in the string, and using driver functions `.SetValue` or `.SetRawValue` is mandatory to do that. +`TemplateRecord` uses [Go templates](https://pkg.go.dev/text/template) to change data. However, while the [Template transformer](./template.md) operates with a single column and automatically applies results, the `TemplateRecord` transformer can make changes to a set of columns in the string, and using driver functions `.SetValue` or `.SetRawValue` is mandatory to do that. With the `TemplateRecord` transformer, you can implement complicated transformation logic using basic or custom template functions. Below you can get familiar with the basic template functions for the `TemplateRecord` transformer. For more information about available custom template functions, see [Custom functions](custom_functions/index.md). diff --git a/docs/built_in_transformers/dynamic_parameters.md b/docs/built_in_transformers/dynamic_parameters.md new file mode 100644 index 00000000..a1a98da1 --- /dev/null +++ b/docs/built_in_transformers/dynamic_parameters.md @@ -0,0 +1,136 @@ +# Dynamic parameters + +## Description + +Most transformers in Greenmask have dynamic parameters. This functionality is possible because Greenmask utilizes a +database driver that can encode and decode raw values into their actual type representations. + +This allows you to retrieve parameter values directly from the records. This capability is particularly beneficial when +you need to resolve functional dependencies between fields or satisfy constraints. Greenmask processes transformations +sequentially. Therefore, when you reference a field that was transformed in a previous step, you will access the +transformed value. + +## Definition + +```yaml +dynamic_params: + - column: "column_name" # (1) + cast_to: "cast_function" # (2) + template: "template_function" # (3) + default_value: any # (4) +``` + +1. Name of the column from which the value is retrieved. +2. Function used to cast the column value to the desired type. +3. Default value used if the column's value is `NULL`. +4. Template used for casting the column value to the desired type. + +## Dynamic parameter options + +* `column` - Specifies the column name. The value from each record in this column will be passed to the transformer as a + parameter. + +* `cast_to` - Indicates the function used to cast the column value to the desired type. Before being passed to the + transformer, the value is cast to this type. For more details, see [Cast functions](#cast-functions). + +* `template` - Defines the template used for casting the column value to the desired type. You can create your own + template and incorporate predefined functions and operators to implement the casting logic or other logic required for + passing the value to the transformer. For more details, + see [Template functions](advanced_transformers/custom_functions/index.md). + +* `default_value` - Determines the default value used if the column's value is `NULL`. This value is represented in raw + format appropriate to the type specified in the `column` parameter. + +## Cast functions + +| name | description | input type | output type | +|------------------------|--------------------------------------------------------------------------------|-------------------------------------------|-------------------------------------------| +| UnixNanoToDate | Cast int value as Unix Timestamp in *Nano Seconds* to *date* type | int2, int4, int8, numeric, float4, float8 | date | +| UnixMicroToDate | Cast int value as Unix Timestamp in *Micro Seconds* to *date* type | int2, int4, int8, numeric, float4, float8 | date | +| UnixMilliToDate | Cast int value as Unix Timestamp in *Milli Seconds* to *date* type | int2, int4, int8, numeric, float4, float8 | date | +| UnixSecToDate | Cast int value as Unix Timestamp in *Seconds* to *date* type | int2, int4, int8, numeric, float4, float8 | date | +| UnixNanoToTimestamp | Cast int value as Unix Timestamp in *Nano Seconds* to *timestamp* type | int2, int4, int8, numeric, float4, float8 | timestamp | +| UnixMicroToTimestamp | Cast int value as Unix Timestamp in *Micro Seconds* to *timestamp* type | int2, int4, int8, numeric, float4, float8 | timestamp | +| UnixMilliToTimestamp | Cast int value as Unix Timestamp in *Milli Seconds* to *timestamp* type | int2, int4, int8, numeric, float4, float8 | timestamp | +| UnixSecToTimestamp | Cast int value as Unix Timestamp in *Seconds* to *timestamp* type | int2, int4, int8, numeric, float4, float8 | timestamp | +| UnixNanoToTimestampTz | Cast int value as Unix Timestamp in *Nano Seconds* to *timestamptz* type | int2, int4, int8, numeric, float4, float8 | timestamptz | +| UnixMicroToTimestampTz | Cast int value as Unix Timestamp in *Micro Seconds* to *timestamptz* type | int2, int4, int8, numeric, float4, float8 | timestamptz | +| UnixMilliToTimestampTz | Cast int value as Unix Timestamp in *Milli Seconds* to *timestamptz* type | int2, int4, int8, numeric, float4, float8 | timestamptz | +| UnixSecToTimestampTz | Cast int value as Unix Timestamp in *Seconds* to *timestamptz* type | int2, int4, int8, numeric, float4, float8 | timestamptz | +| DateToUnixNano | Cast *date* value to *int* value as a Unix Timestamp in *Nano Seconds* | date | int2, int4, int8, numeric, float4, float8 | +| DateToUnixMicro | Cast *date* value to *int* value as a Unix Timestamp in *Micro Seconds* | date | int2, int4, int8, numeric, float4, float8 | +| DateToUnixMilli | Cast *date* value to *int* value as a Unix Timestamp in *Milli Seconds* | date | int2, int4, int8, numeric, float4, float8 | +| DateToUnixSec | Cast *date* value to *int* value as a Unix Timestamp in *Seconds* | date | int2, int4, int8, numeric, float4, float8 | +| TimestampToUnixNano | Cast *timestamp* value to *int* value as a Unix Timestamp in *Nano Seconds* | timestamp | int2, int4, int8, numeric, float4, float8 | +| TimestampToUnixMicro | Cast *timestamp* value to *int* value as a Unix Timestamp in *Micro Seconds* | timestamp | int2, int4, int8, numeric, float4, float8 | +| TimestampToUnixMilli | Cast *timestamp* value to *int* value as a Unix Timestamp in *Milli Seconds* | timestamp | int2, int4, int8, numeric, float4, float8 | +| TimestampToUnixSec | Cast *timestamp* value to *int* value as a Unix Timestamp in *Seconds* | timestamp | int2, int4, int8, numeric, float4, float8 | +| TimestampTzToUnixNano | Cast *timestamptz* value to *int* value as a Unix Timestamp in *Nano Seconds* | timestamptz | int2, int4, int8, numeric, float4, float8 | +| TimestampTzToUnixMicro | Cast *timestamptz* value to *int* value as a Unix Timestamp in *Micro Seconds* | timestamptz | int2, int4, int8, numeric, float4, float8 | +| TimestampTzToUnixMilli | Cast *timestamptz* value to *int* value as a Unix Timestamp in *Milli Seconds* | timestamptz | int2, int4, int8, numeric, float4, float8 | +| TimestampTzToUnixSec | Cast *timestamptz* value to *int* value as a Unix Timestamp in *Seconds* | timestamptz | int2, int4, int8, numeric, float4, float8 | +| FloatToInt | Cast float value to one of integer type. The fractional part will be discarded | numeric, float4, float8 | int2, int4, int8, numeric | +| IntToFloat | Cast int value to one of integer type | int2, int4, int8, numeric | numeric, float4, float8 | +| IntToBool | Cast int value to boolean. The value with 0 is false, 1 is true | int2, int4, int8, numeric, float4, float8 | bool | +| BoolToInt | Cast boolean value to int. The value false is 0, true is 1 | bool | int2, int4, int8, numeric, float4, float8 | + +## Example: Functional dependency resolution between columns + +There is simplified schema of the table `humanresources.employee` from the [playground](../playground.md): + +```sql + Column | Type +------------------+----------------------------- + businessentityid | integer + jobtitle | character varying(50) + birthdate | date + hiredate | date +Check constraints: + CHECK (birthdate >= '1930-01-01'::date AND birthdate <= (now() - '18 years'::interval)) +``` + +As you can see, there is a functional dependency between the `birthdate` and `hiredate` columns. Logically, +the `hiredate` should be later than the `birthdate`. Additionally, the `birthdate` should range from `1930-01-01` +to `18` years prior to the current date. + +Imagine that you need to generate random `birthdate` and `hiredate` columns. To ensure these dates satisfy the +constraints, you can use dynamic parameters in the `RandomDate` transformer: + +```yaml +- schema: "humanresources" + name: "employee" + transformers: + + - name: "RandomDate" # (1) + params: + column: "birthdate" + min: '{{ now | tsModify "-30 years" | .EncodeValue }}' # (2) + max: '{{ now | tsModify "-18 years" | .EncodeValue }}' # (3) + + - name: "RandomDate" # (4) + params: + column: "hiredate" + max: "{{ now | .EncodeValue }}" # (5) + dynamic_params: + min: + column: "birthdate" # (6) + template: '{{ .GetValue | tsModify "18 years" | .EncodeValue }}' # (7) +``` + +1. Firstly we generate the `RadnomDate` for birthdate column. The result of the transformation will used as the minimum + value for the next transformation for `hiredate` column. +2. Apply the template for static parameter. It calculates the now date and subtracts `30` years from it. The result + is `1994`. The function tsModify return not a raw data, but time.Time object. For getting the raw value suitable for + birthdate type we need to pass this value to `.EncodeValue` function. This value is used as the minimum value for + the `birthdate` column. +3. The same as the previous step, but we subtract `18` years from the now date. The result is `2002`. +4. Generate the `RadnomDate` for `hiredate` column based on the value from the `birthdate`. +5. Set the maximum value for the `hiredate` column. The value is the current date. +6. The `min` parameter is set to the value of the `birthdate` column from the previous step. +7. The template gets the value of the randomly generated `birthdate` value and adds `18` years to it. + +Below is the result of the transformation: + +![img.png](../assets/built_in_transformers/img.png) + +From the result, you can see that all functional dependencies and constraints are satisfied. diff --git a/docs/built_in_transformers/index.md b/docs/built_in_transformers/index.md index 31fda4ed..e2c2adb5 100644 --- a/docs/built_in_transformers/index.md +++ b/docs/built_in_transformers/index.md @@ -1,7 +1,13 @@ # About transformers -Transformers in Greenmask are methods which are applied to obfuscate sensitive data. All Greenmask transformers are split into the following groups: +Transformers in Greenmask are methods which are applied to obfuscate sensitive data. All Greenmask transformers are +split into the following groups: +- [Transformation engines](transformation_engines.md) — the type of generator used in transformers. Hash (deterministic) + and random (randomization) +- [Dynamic parameters](dynamic_parameters.md) — transformers that require an input of parameters and generate + random data based on them. - [Standard transformers](standard_transformers/index.md) — transformers that require only an input of parameters. -- [Advanced transformers](advanced_transformers/index.md) — transformers that can be modified according to user's needs with the help of [custom functions](advanced_transformers/custom_functions/index.md). +- [Advanced transformers](advanced_transformers/index.md) — transformers that can be modified according to user's needs + with the help of [custom functions](advanced_transformers/custom_functions/index.md). - Custom transformers — coming soon... diff --git a/docs/built_in_transformers/transformation_engines.md b/docs/built_in_transformers/transformation_engines.md new file mode 100644 index 00000000..72df5ac6 --- /dev/null +++ b/docs/built_in_transformers/transformation_engines.md @@ -0,0 +1 @@ +# Transformation engine diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 050d9ec0..00000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,408 +0,0 @@ -# Getting started - -This guide will help you to quickly get familiar with Greenmask by setting up **Greenmask Playground** and trying it in action. Greenmask Playground is a Docker Compose environment that includes the following components: - -* **Original database** — the source database you'll be working with. -* **Empty database for restoration** — an empty database where the restored data will be placed. -* **MinIO storage** — used for storage purposes. -* **Greenmask Utility** — Greenmask itself, ready for use. - -!!! warning - - To complete this guide, you must have **Docker** and **docker-compose** installed. - -## Setting up Greenmask Playground - -1. Clone the `greenmask` repository and navigate to its directory by running the following commands: - - ```shell - git clone git@github.com:GreenmaskIO/greenmask.git && cd greenmask - ``` - -2. Once you have cloned the repository, start the environment by running Docker Compose: - - ```shell - docker-compose run greenmask - ``` -!!! Tip - - If you're experiencing problems with pulling images from Docker Hub, you can build the Greenmask image from source by running the following command: - - ```shell - docker-compose run greenmask-from-source - ``` - -Now you have Greenmask Playground up and running with a shell prompt inside the container. All further operations will be carried out within this container's shell. - -## Commands - -Before proceeding to configure Greenmask, let us explore some of the available Greenmask commands: - -```shell -greenmask - --log-format=[json|text] \ - --log-level=[debug|info|error] \ - --config=config.yml \ - [dump | list-dumps | delete | list-transformers | restore | show-dump | validate | completion] -``` - -Below you can find a description for each command: - -* `dump` — performs a logical data dump, transforms the data, and stores it in the designated storage. - -* `list-dumps` — retrieves a list of all stored dumps within the chosen storage. - -* `delete` — removes a dump with a specific ID from the storage. - -* `list-transformers` — displays a list of approved transformers and their documentation. - -* `restore` — restores a dump either by specifying its ID or using the latest available dump to the target database. - -* `show-dump` — presents metadata information about a specific dump (equivalent to `pg_restore -l ./`). - -* `validate` — executes a validation process and generates a data diff for the transformation. - -* `completion` — generates the autocompletion script for the specified shell. - -Note that you can customize the logging format and level using the provided options. Specifying a configuration file (`config.yml`) is mandatory to guide the tool's behavior. - -## Building config.yml - -### The sample database - -Greenmask Playground uses the [Microsoft AdventureWorks sample databases](https://learn.microsoft.com/en-us/sql/samples/adventureworks-install-configure?view=sql-server-ver16&tabs=ssms), -that have been ported to PostgreSQL and sourced from [morenoh149/postgresDBSamples](https://github.com/morenoh149/postgresDBSamples). - -Within Playground, you'll find two predefined databases: - -```text - Name | Owner --------------+---------- - original | postgres - transformed | postgres -``` - -where: - -* `original` — a database that contains the deployed AdventureWorks sample databases as-is. -* `transformed` — an empty database for restoring transformed dumps. - -Within the Greenmask container, you'll have access to the following commands: - -* `greenmask` — launches the Greenmask obfuscation utility. -* `psql_o` — connects to the `original` database using the psql utility. -* `psql_t` — connects to the `transformed` database using the psql utility. -* `cleanup` — drops and recreates the `transformed` database as an empty container. - -If you are using an external Integrated Development Environment (IDE), you can connect using the following URIs: - -* Original database: `postgresql://postgres:example@localhost:54316/original` -* Transformed database: `postgresql://postgres:example@localhost:54316/transformed` - -### Creating a simple configuration - -The Greenmask utility container is configured with a volume attached to the `./playground` directory located at the root of the repository. Within this directory, there is a pre-defined configuration file called `config.yml`. You have the flexibility to modify this configuration as needed, making adjustments or adding additional transformations. - -Any changes made to this configuration file will be accessible within the container, allowing you to tailor Greenmask's behavior to your specific requirements. - -To build a basic configuration for Greenmask, you can follow these steps: - -1. Get a list of currently available transformers by running the following command: - - ```shell - greenmask --config config.yml list-transformers - ``` - - ![list-transformers-example.png](assets%2Fgetting_started%2Flist-transformers-example.png) - -When building your configuration, ensure that you fill in all the required attributes, including the following sections: - -* common -* storage -* dump -* restore - -Below is an example of a minimal configuration in YAML format: - -```yaml -common: - pg_bin_path: "/usr/lib/postgresql/16/bin" - tmp_dir: "/tmp" - -storage: - s3: - endpoint: "http://playground-storage:9000" - bucket: "adventureworks" - region: "us-east-1" - access_key_id: "Q3AM3UQ867SPQQA43P2F" - secret_access_key: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" - -validate: -# resolved_warnings: -# - "aa808fb574a1359c6606e464833feceb" - -dump: - pg_dump_options: # pg_dump option that will be provided - dbname: "host=playground-db user=postgres password=example dbname=original" - jobs: 10 - - transformation: # List of tables to transform - - schema: "humanresources" # Table schema - name: "employee" # Table name - transformers: # List of transformers to apply - - name: "NoiseDate" # name of transformers - params: # Transformer parameters - ratio: "10 year 9 mon 1 day" - column: "birthdate" # Column parameter - this transformer affects scheduled_departure column - -restore: - pg_restore_options: # pg_restore option (you can use the same options as pg_restore has) - jobs: 10 - dbname: "host=playground-db user=postgres password=example dbname=transformed" -``` - -This example demonstrates the essential components of a Greenmask configuration file in YAML format. Please ensure that -you customize it according to your specific needs. - -In the config above applied only one transformer on table `humanresources.employee` called `NoiseDate` with the -next parameters: - -* ratio - add noise to the value up to "10 year 9 mon 1 day" eather before or after. For he current value is - `1976-12-03` and the transformer generated the noise value randomly `1 year 3 mon` and decided to increase that value. - The result will be `1978-02-033` -* column - there is a column name that is going to be affected called `birthdate` - -### Run validation procedure - -You can utilize the following command to initiate a validation procedure: - -```shell -greenmask --config config.yml validate \ - --data \ - --diff \ - --format=vertical \ - --rows-limit=2 -``` - -The validation result will be displayed as follows: - -![validate-result.png](assets%2Fgetting_started%2Fvalidate-result.png) - -There is one warning; let's investigate it: - -```yaml -{ - "hash": "aa808fb574a1359c6606e464833feceb", - "meta": { - "ColumnName": "birthdate", - "ConstraintDef": "CHECK (birthdate >= '1930-01-01'::date AND birthdate <= (now() - '18 years'::interval))", - "ConstraintName": "humanresources", - "ConstraintSchema": "humanresources", - "ConstraintType": "Check", - "ParameterName": "column", - "SchemaName": "humanresources", - "TableName": "employee", - "TransformerName": "NoiseDate" - }, - "msg": "possible constraint violation: column has Check constraint", - "severity": "warning" -} -``` - -The validation warnings include the following details: - -* **hash** - A unique identifier for each validation warning, which can be used to exclude the warning from future - checks - by adding it to the `validate.resolved_warnings` configuration. -* **meta** - Contains essential information that helps identify the location in the configuration or the potentially - violated constraint. -* **msg** - A comprehensive message that provides a detailed explanation of the warning's cause -* **severity** - Indicates the severity of the warning, which can be either "warning" or "error." In the case of an - error, Greenmask will exit immediately with a non-zero exit code. - -The next step in the validation procedure is to compare the data before and after the transformation. This comparison -is presented in a table format. Columns with a red background indicate that they have been affected by the -transformation. The green values represent the original data before the transformation, while the red values depict -the data after the transformation. - -To exclude a warning from future runs, you can uncomment the resolved_warning attribute in the configuration file. - -```yaml -validate: - resolved_warnings: - - "aa808fb574a1359c6606e464833feceb" -``` - -By adding the hash of a warning to the `validate.resolved_warnings` configuration in your `config.yml` -file, you can effectively exclude that specific warning from being displayed in subsequent runs of the validation -process using the command: - -```shell -greenmask --config config.yml validate -``` - -### Dumping procedure - -To perform the data dumping procedure, follow these steps: - -1. Execute the following command to initiate the dump using your configured settings: - ```shell - greenmask --config config.yml dump - ``` - -2. Once the dumping process is complete, you will find the dump with an associated ID in the designated storage. - To list all available dumps, use the following command: - ```shell - greenmask --config config.yml list-dumps - ``` - ![list-dumps.png](assets%2Fgetting_started%2Flist-dumps.png) - -3. If you wish to examine the data that is scheduled for restoration, you can use the show dump command. - Provide the `dumpId` in your call to access the details: - - ```shell - greenmask --config config.yml show-dump 1702489882319 - ``` - - In the output below, you can observe the portion of objects that will be restored: - - ```text - ; - ; Archive created at 2023-12-13 17:51:22 UTC - ; dbname: original - ; TOC Entries: 986 - ; Compression: 0 - ; Dump Version: 16.1 (Ubuntu 16.1-1.pgdg22.04+1) - ; Format: DIRECTORY - ; Integer: 4 bytes - ; Offset: 8 bytes - ; Dumped from database version: 16.0 (Debian 16.0-1.pgdg120+1) - ; Dumped by pg_dump version: 16.1 (Ubuntu 16.1-1.pgdg22.04+1) - ; - ; - ; Selected TOC Entries: - ; - 4666; 0 0 ENCODING - ENCODING - 4667; 0 0 STDSTRINGS - STDSTRINGS - 4668; 0 0 SEARCHPATH - SEARCHPATH - 4669; 1262 16384 DATABASE - original postgres - 14; 2615 18396 SCHEMA - hr postgres - 9; 2615 16524 SCHEMA - humanresources postgres - 4670; 0 0 COMMENT - SCHEMA humanresources postgres - 13; 2615 18343 SCHEMA - pe postgres - 8; 2615 16429 SCHEMA - person postgres - 4671; 0 0 COMMENT - SCHEMA person postgres - 15; 2615 18421 SCHEMA - pr postgres - 10; 2615 16586 SCHEMA - production postgres - 4672; 0 0 COMMENT - SCHEMA production postgres - 16; 2615 18523 SCHEMA - pu postgres - 11; 2615 17034 SCHEMA - purchasing postgres - 4673; 0 0 COMMENT - SCHEMA purchasing postgres - - ... - ... - ... - 4427; 2606 18157 FK CONSTRAINT sales shoppingcartitem FK_ShoppingCartItem_Product_ProductID postgres - 4428; 2606 18162 FK CONSTRAINT sales specialofferproduct FK_SpecialOfferProduct_Product_ProductID postgres - 4429; 2606 18167 FK CONSTRAINT sales specialofferproduct FK_SpecialOfferProduct_SpecialOffer_SpecialOfferID postgres - 4430; 2606 18182 FK CONSTRAINT sales store FK_Store_BusinessEntity_BusinessEntityID postgres - 4431; 2606 18187 FK CONSTRAINT sales store FK_Store_SalesPerson_SalesPersonID postgres - - ``` - -### Restoration Procedure - -To restore data to the target database, you can use the following commands: - -1. To restore data from a specific dump (identified by its **dumpId**), execute the following command: - ```shell - greenmask --config config.yml restore [dumpId] - ``` - Replace **[dumpId]** with the appropriate dump identifier. - -2. Alternatively, you can restore the latest available dump by using the reserved word **latest** like this: - ```shell - greenmask --config config.yml restore latest - ``` - -3. After the restoration process is complete, you can verify the restored data by running the following PostgreSQL - command: - ```shell - psql_t -xc 'select * from humanresources.employee limit 2;' - ``` - This command will display the first two rows of the "flights" table in the target database, showing the restored - data. - - ``` - -[ RECORD 1 ]----+------------------------------------- - businessentityid | 1 - nationalidnumber | 295847284 - loginid | adventure-works\ken0 - jobtitle | Chief Executive Officer - birthdate | 1968-12-18 - maritalstatus | S - gender | M - hiredate | 2009-01-14 - salariedflag | t - vacationhours | 99 - sickleavehours | 69 - currentflag | t - rowguid | f01251e5-96a3-448d-981e-0f99d789110d - modifieddate | 2014-06-30 00:00:00 - organizationnode | / - -[ RECORD 2 ]----+------------------------------------- - businessentityid | 2 - nationalidnumber | 245797967 - loginid | adventure-works\terri0 - jobtitle | Vice President of Engineering - birthdate | 1970-05-04 - maritalstatus | S - gender | F - hiredate | 2008-01-31 - salariedflag | t - vacationhours | 1 - sickleavehours | 20 - currentflag | t - rowguid | 45e8f437-670d-4409-93cb-f9424a40d6ee - modifieddate | 2014-06-30 00:00:00 - organizationnode | /1/ - - ``` - -### Deleting a Dump - -To remove a specific dump from the storage, use the **delete** command with the appropriate **dumpId**. -Here's how to do it: - -```shell -greenmask --config config.yml delete 1702489882319 -``` - -After executing this command, the specified dump will be deleted from the storage. - -To verify the changes, you can list the available dumps using the following command: - -The result - -```shell -greenmask --config config.yml list-dumps -``` - -The list displayed dumps will not include the deleted dump with the previously provided dumpId. - -``` -+----+------+----------+------+-----------------+----------+-------------+--------+ -| ID | DATE | DATABASE | SIZE | COMPRESSED SIZE | DURATION | TRANSFORMED | STATUS | -+----+------+----------+------+-----------------+----------+-------------+--------+ -+----+------+----------+------+-----------------+----------+-------------+--------+ -``` - -## Conclusion - -This is a straightforward example of using Greenmask. If you wish to explore more advanced transformation cases and -delve deeper into the documentation. - -Additionally, if you have any questions or require further assistance, don't hesitate to reach out via -[Discord](https://discord.gg/97AKHdGD), [Telegram](https://t.me/greenmask_community), or by -emailing us at [support@greenmask.io](mailto:support@greenmask.io). Our team is here to help and -provide guidance as needed. diff --git a/mkdocs.yml b/mkdocs.yml index c6574dfc..f3069b84 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,7 +14,7 @@ theme: - content.tooltips # - navigation.sections - navigation.tabs - # - navigation.top + - navigation.top # - navigation.tracking - search.highlight - search.share @@ -53,6 +53,8 @@ nav: - Commands: commands.md - Transformers: - built_in_transformers/index.md + - Dynamic parameters: built_in_transformers/dynamic_parameters.md + - Transformation engines: built_in_transformers/transformation_engines.md - Standard transformers: - built_in_transformers/standard_transformers/index.md - Cmd: built_in_transformers/standard_transformers/cmd.md From 6e04dcaeaeb2297d5a0e888a827bbf5b90331b15 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Sun, 12 May 2024 23:09:28 +0300 Subject: [PATCH 02/20] [docs] Added two points in key features list. Removed old artifacts --- README.md | 9 +- docs/index.md | 49 ++- docs/resources/list-dumps.png | Bin 8111 -> 0 bytes docs/resources/list-transformers-example.png | Bin 60568 -> 0 bytes docs/resources/validate-result.png | Bin 60751 -> 0 bytes getting_started.md | 412 ------------------- 6 files changed, 43 insertions(+), 427 deletions(-) delete mode 100644 docs/resources/list-dumps.png delete mode 100644 docs/resources/list-transformers-example.png delete mode 100644 docs/resources/validate-result.png delete mode 100644 getting_started.md diff --git a/README.md b/README.md index 2baaaaa9..c1b9b224 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ backward-compatible with existing PostgreSQL utilities. # Features +* **Deterministic transformers** — deterministic approach to data transformation based on the hash + functions. This ensures that the same input data will always produce the same output data. Almost each transformer + supports either `random` or `hash` engine making it universal for any use case. +* **Dynamic parameters** — almost each transformer supports dynamic parameters, allowing to parametrize the + transformer dynamically from the table column value. This is helpful for resolving the functional dependencies + between columns and satisfying the constraints. * **Cross-platform** - Can be easily built and executed on any platform, thanks to its Go-based architecture, which eliminates platform dependencies. * **Database type safe** - Ensures data integrity by validating data and utilizing the database driver for @@ -52,9 +58,6 @@ solution for managing obfuscation procedures. We recognize the challenges of mai throughout the software lifecycle. Greenmask is dedicated to providing valuable tools and features that ensure the obfuscation process remains fresh, predictable, and transparent. -## [Getting started](./getting_started.md) - - ### General Information It is evident that the most appropriate approach for executing logical backup dumping and restoration is by leveraging diff --git a/docs/index.md b/docs/index.md index 9ed4a425..ed87f610 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,34 +3,59 @@ **Greenmask** is a powerful open-source utility that is designed for logical database backup dumping, obfuscation, and restoration. It offers extensive functionality for backup, anonymization, and data masking. -Greenmask is written in pure Go and includes ported PostgreSQL libraries that allows for platform independence. This tool is stateless and does not require any changes to your database schema. It is designed to be highly customizable and backward-compatible with existing PostgreSQL utilities. +Greenmask is written in pure Go and includes ported PostgreSQL libraries that allows for platform independence. This +tool is stateless and does not require any changes to your database schema. It is designed to be highly customizable and +backward-compatible with existing PostgreSQL utilities. ## Purpose -The Greenmask utility plays a central role in the Greenmask ecosystem. Our goal is to develop a comprehensive, UI-based solution for managing obfuscation procedures. We recognize the challenges of maintaining obfuscation consistency throughout the software lifecycle. Greenmask is dedicated to providing valuable tools and features that ensure the obfuscation process remains fresh, predictable, and transparent. +The Greenmask utility plays a central role in the Greenmask ecosystem. Our goal is to develop a comprehensive, UI-based +solution for managing obfuscation procedures. We recognize the challenges of maintaining obfuscation consistency +throughout the software lifecycle. Greenmask is dedicated to providing valuable tools and features that ensure the +obfuscation process remains fresh, predictable, and transparent. ## Key features +* **Deterministic transformers** — deterministic approach to data transformation based on the hash + functions. This ensures that the same input data will always produce the same output data. Almost each transformer + supports either `random` or `hash` engine making it universal for any use case. +* **Dynamic parameters** — almost each transformer supports dynamic parameters, allowing to parametrize the + transformer dynamically from the table column value. This is helpful for resolving the functional dependencies + between columns and satisfying the constraints. * **Cross-platform** — can be easily built and executed on any platform, thanks to its Go-based architecture, which eliminates platform dependencies. * **Database type safe** — ensures data integrity by validating data and utilizing the database driver for encoding and decoding operations. This approach guarantees the preservation of data formats. -* **Transformation validation and easy maintainable** — during obfuscation development, Greenmask provides validation warnings and a transformation diff feature, allowing you to monitor and maintain transformations effectively throughout the software lifecycle. -* **Partitioned tables transformation inheritance** — define transformation configurations once and apply them to all partitions within partitioned tables, simplifying the obfuscation process. +* **Transformation validation and easy maintainable** — during obfuscation development, Greenmask provides validation + warnings and a transformation diff feature, allowing you to monitor and maintain transformations effectively + throughout the software lifecycle. +* **Partitioned tables transformation inheritance** — define transformation configurations once and apply them to all + partitions within partitioned tables, simplifying the obfuscation process. * **Stateless** — Greenmask operates as a logical dump and does not impact your existing database schema. -* **Backward compatible** — it fully supports the same features and protocols as existing vanilla PostgreSQL utilities. Dumps created by Greenmask can be successfully restored using the pg_restore utility. -* **Extensible** — users have the flexibility to implement domain-based transformations in any programming language or use predefined templates. -* **Declarative** — Greenmask allows you to define configurations in a structured, easily parsed, and recognizable format. -* **Integrable** — integrate Greenmask seamlessly into your CI/CD system for automated database obfuscation and restoration. -* **Parallel execution** — take advantage of parallel dumping and restoration, significantly reducing the time required to deliver results. -* **Provide variety of storages** — Greenmask offers a variety of storage options for local and remote data storage, including directories and S3-like storage solutions. +* **Backward compatible** — it fully supports the same features and protocols as existing vanilla PostgreSQL utilities. + Dumps created by Greenmask can be successfully restored using the pg_restore utility. +* **Extensible** — users have the flexibility to implement domain-based transformations in any programming language or + use predefined templates. +* **Declarative** — Greenmask allows you to define configurations in a structured, easily parsed, and recognizable + format. +* **Integrable** — integrate Greenmask seamlessly into your CI/CD system for automated database obfuscation and + restoration. +* **Parallel execution** — take advantage of parallel dumping and restoration, significantly reducing the time required + to deliver results. +* **Provide variety of storages** — Greenmask offers a variety of storage options for local and remote data storage, + including directories and S3-like storage solutions. ## Use cases Greenmask is ideal for various scenarios, including: -* **Backup and restoration**. Use Greenmask for your daily routines involving logical backup dumping and restoration. It seamlessly handles tasks like table restoration after truncation. Its functionality closely mirrors that of pg_dump and pg_restore, making it a straightforward replacement. -* **Anonymization, transformation, and data masking**. Employ Greenmask for anonymizing, transforming, and masking backups, especially when setting up a staging environment or for analytical purposes. It simplifies the deployment of a pre-production environment with consistently anonymized data, facilitating faster time-to-market in the development lifecycle. +* **Backup and restoration**. Use Greenmask for your daily routines involving logical backup dumping and restoration. It + seamlessly handles tasks like table restoration after truncation. Its functionality closely mirrors that of pg_dump + and pg_restore, making it a straightforward replacement. +* **Anonymization, transformation, and data masking**. Employ Greenmask for anonymizing, transforming, and masking + backups, especially when setting up a staging environment or for analytical purposes. It simplifies the deployment of + a pre-production environment with consistently anonymized data, facilitating faster time-to-market in the development + lifecycle. ## Links diff --git a/docs/resources/list-dumps.png b/docs/resources/list-dumps.png deleted file mode 100644 index 2f16c565a516ee4097949385d7c4eccf3510e72a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8111 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 diff --git a/docs/resources/validate-result.png b/docs/resources/validate-result.png deleted file mode 100644 index d41029803f7503d5aaba20a22a8d53ca28325637..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60751 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 diff --git a/getting_started.md b/getting_started.md deleted file mode 100644 index 0e4a1dec..00000000 --- a/getting_started.md +++ /dev/null @@ -1,412 +0,0 @@ -# Getting started - -This page will walk you through the easiest way to get started with Greenmask for the first time. To complete this -guide, you will need to have **Docker** and **docker-compose** installed. - -## About the Playground - -The **Greenmask Playground** is a Docker Compose environment that includes the following components: - -* **Original Database**: This is the source database you'll be working with. -* **Empty Database for Restoration**: An empty database where the restored data will be placed. -* **MinIO Storage**: Used for storage purposes. -* **Greenmask Utility**: The Greenmask tool itself, ready for use. - -## Starting the Playground - -To begin, follow these steps to set up the Greenmask Playground: - -Clone the `greenmask` repository and navigate to the `greenmask` directory using the following commands: - -```shell -git clone git@github.com:GreenmaskIO/greenmask.git && cd greenmask -``` - -Once you have cloned the repository, run `docker-compose` to start the environment: - -```shell -docker-compose run greenmask -``` - -If you're experiencing problems with pulling images from Docker Hub, you can build the Greenmask image from source by -executing the following command: - -```shell -docker-compose run greenmask-from-source -``` - -After executing these commands, you will have Greenmask up and running with a shell prompt inside the container. All -further operations will be carried out within this container's shell. - -## Commands - -Before proceeding to configure Greenmask, let's explore some of the available Greenmask commands. - -```shell -greenmask - --log-format=[json|text] \ - --log-level=[debug|info|error] \ - --config=config.yml \ - [dump | list-dumps | delete | list-transformers | restore | show-dump | validate | completion] -``` - -Available Actions and Descriptions: - -* **list-transformers**: Display a list of approved transformers along with their corresponding documentation. - -* **validate**: Execute a validation process and generate a data diff for the transformation. - -* **dump**: Perform a logical data dump, transform the data, and store it in the designated storage. - -* **list-dumps**: Retrieve a list of all stored dumps within the chosen storage. - -* **show-dump**: Present metadata information about a specific dump (equivalent to `pg_restore -l ./`). - -* **restore**: Restore a dump either by specifying its ID or using the latest available dump to the target database. - -* **delete**: Remove a dump with a specific ID from the storage. - -* **completion**: Generate the autocompletion script for the specified shell - -Please note that you can customize the logging format and level using the provided options, and you **must specify** a -configuration file (config.yml) to guide the tool's behavior. - -## Building config - -### The sample database - -The Greenmask Playground utilizes -the [Microsoft AdventureWorks sample databases](https://learn.microsoft.com/en-us/sql/samples/adventureworks-install-configure?view=sql-server-ver16&tabs=ssms), -which have been ported to PostgreSQL and sourced -from [morenoh149/postgresDBSamples](https://github.com/morenoh149/postgresDBSamples). - -Within the playground, you'll find two predefined databases: - -```text - Name | Owner --------------+---------- - original | postgres - transformed | postgres -``` - -* **original** - This database contains the deployed AdventureWorks sample databases as-is. -* **transformed** - An empty database intended for restoring transformed dumps. - -Within the **Greenmask container**, you'll have access to the following commands, which will be used throughout this -guide: - -* `greenmask` - Launches the Greenmask obfuscation tool utility. -* `psql_o` - Connects to the `original` database using the psql utility. -* `psql_t` - Connects to the `transformed` database using the psql utility. -* `cleanup` - dDrops and recreates the `transformed` database as an empty container. - -If you are using an external Integrated Development Environment (IDE), you can connect using the following connection -URIs: - -* Original database: `postgresql://postgres:example@localhost:54316/original` -* Transformed database: `postgresql://postgres:example@localhost:54316/transformed` - -### Creating a Simple Configuration - -The Greenmask utility container is configured with a volume attached to the `./playground` directory located at the root -of the repository. Within this directory, there is a pre-defined configuration file called `config.yml`. You have the -flexibility to modify this configuration as needed, making adjustments or adding additional transformations. - -Any changes made to this configuration file will be accessible within the container, allowing you to tailor Greenmask's -behavior to your specific requirements. - -To build a basic configuration for Greenmask, you can follow these steps: - -1. Obtain a list of currently available transformers by using the following command: - -```shell -greenmask --config config.yml list-transformers -``` - -![img.png](docs/resources/list-transformers-example.png) - -When building your configuration, ensure that you fill in all the required attributes, including the following sections: - -* common -* storage -* dump -* restore - -Below is an example of a minimal configuration in YAML format: - -```yaml -common: - pg_bin_path: "/usr/lib/postgresql/16/bin" - tmp_dir: "/tmp" - -storage: - s3: - endpoint: "http://playground-storage:9000" - bucket: "adventureworks" - region: "us-east-1" - access_key_id: "Q3AM3UQ867SPQQA43P2F" - secret_access_key: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" - -validate: -# resolved_warnings: -# - "aa808fb574a1359c6606e464833feceb" - -dump: - pg_dump_options: # pg_dump option that will be provided - dbname: "host=playground-db user=postgres password=example dbname=original" - jobs: 10 - - transformation: # List of tables to transform - - schema: "humanresources" # Table schema - name: "employee" # Table name - transformers: # List of transformers to apply - - name: "NoiseDate" # name of transformers - params: # Transformer parameters - ratio: "10 year 9 mon 1 day" - column: "birthdate" # Column parameter - this transformer affects scheduled_departure column - -restore: - pg_restore_options: # pg_restore option (you can use the same options as pg_restore has) - jobs: 10 - dbname: "host=playground-db user=postgres password=example dbname=transformed" -``` - -This example demonstrates the essential components of a Greenmask configuration file in YAML format. Please ensure that -you customize it according to your specific needs. - -In the config above applied only one transformer on table `humanresources.employee` called `NoiseDate` with the -next parameters: - -* ratio - add noise to the value up to "10 year 9 mon 1 day" eather before or after. For he current value is - `1976-12-03` and the transformer generated the noise value randomly `1 year 3 mon` and decided to increase that value. - The result will be `1978-02-033` -* column - there is a column name that is going to be affected called `birthdate` - -### Run validation procedure - -You can utilize the following command to initiate a validation procedure: - -```shell -greenmask --config config.yml validate --data --diff --format vertical --rows-limit=2 -``` - -The validation result will be displayed as follows: - -![img.png](docs/resources/validate-result.png) - -There is one warning; let's investigate it: - -```yaml -{ - "hash": "aa808fb574a1359c6606e464833feceb", - "meta": { - "ColumnName": "birthdate", - "ConstraintDef": "CHECK (birthdate >= '1930-01-01'::date AND birthdate <= (now() - '18 years'::interval))", - "ConstraintName": "humanresources", - "ConstraintSchema": "humanresources", - "ConstraintType": "Check", - "ParameterName": "column", - "SchemaName": "humanresources", - "TableName": "employee", - "TransformerName": "NoiseDate" - }, - "msg": "possible constraint violation: column has Check constraint", - "severity": "warning" -} -``` - -The validation warnings include the following details: - -* **hash** - A unique identifier for each validation warning, which can be used to exclude the warning from future - checks - by adding it to the `validate.resolved_warnings` configuration. -* **meta** - Contains essential information that helps identify the location in the configuration or the potentially - violated constraint. -* **msg** - A comprehensive message that provides a detailed explanation of the warning's cause -* **severity** - Indicates the severity of the warning, which can be either "warning" or "error." In the case of an - error, Greenmask will exit immediately with a non-zero exit code. - -The next step in the validation procedure is to compare the data before and after the transformation. This comparison -is presented in a table format. Columns with a red background indicate that they have been affected by the -transformation. The green values represent the original data before the transformation, while the red values depict -the data after the transformation. - -To exclude a warning from future runs, you can uncomment the resolved_warning attribute in the configuration file. - -```yaml -validate: - resolved_warnings: - - "aa808fb574a1359c6606e464833feceb" -``` - -By adding the hash of a warning to the `validate.resolved_warnings` configuration in your `config.yml` -file, you can effectively exclude that specific warning from being displayed in subsequent runs of the validation -process using the command: - -```shell -greenmask --config config.yml validate -``` - -### Dumping procedure - -To perform the data dumping procedure, follow these steps: - -1. Execute the following command to initiate the dump using your configured settings: - ```shell - greenmask --config config.yml dump - ``` - -2. Once the dumping process is complete, you will find the dump with an associated ID in the designated storage. - To list all available dumps, use the following command: - ```shell - greenmask --config config.yml list-dumps - ``` - ![img.png](docs/resources/list-dumps.png) - -3. If you wish to examine the data that is scheduled for restoration, you can use the show dump command. - Provide the `dumpId` in your call to access the details: - ```shell - greenmask --config config.yml show-dump 1702489882319 - ``` - In the output below, you can observe the portion of objects that will be restored: - ```shell - ; - ; Archive created at 2023-12-13 17:51:22 UTC - ; dbname: original - ; TOC Entries: 986 - ; Compression: 0 - ; Dump Version: 16.1 (Ubuntu 16.1-1.pgdg22.04+1) - ; Format: DIRECTORY - ; Integer: 4 bytes - ; Offset: 8 bytes - ; Dumped from database version: 16.0 (Debian 16.0-1.pgdg120+1) - ; Dumped by pg_dump version: 16.1 (Ubuntu 16.1-1.pgdg22.04+1) - ; - ; - ; Selected TOC Entries: - ; - 4666; 0 0 ENCODING - ENCODING - 4667; 0 0 STDSTRINGS - STDSTRINGS - 4668; 0 0 SEARCHPATH - SEARCHPATH - 4669; 1262 16384 DATABASE - original postgres - 14; 2615 18396 SCHEMA - hr postgres - 9; 2615 16524 SCHEMA - humanresources postgres - 4670; 0 0 COMMENT - SCHEMA humanresources postgres - 13; 2615 18343 SCHEMA - pe postgres - 8; 2615 16429 SCHEMA - person postgres - 4671; 0 0 COMMENT - SCHEMA person postgres - 15; 2615 18421 SCHEMA - pr postgres - 10; 2615 16586 SCHEMA - production postgres - 4672; 0 0 COMMENT - SCHEMA production postgres - 16; 2615 18523 SCHEMA - pu postgres - 11; 2615 17034 SCHEMA - purchasing postgres - 4673; 0 0 COMMENT - SCHEMA purchasing postgres - - ... - ... - ... - 4427; 2606 18157 FK CONSTRAINT sales shoppingcartitem FK_ShoppingCartItem_Product_ProductID postgres - 4428; 2606 18162 FK CONSTRAINT sales specialofferproduct FK_SpecialOfferProduct_Product_ProductID postgres - 4429; 2606 18167 FK CONSTRAINT sales specialofferproduct FK_SpecialOfferProduct_SpecialOffer_SpecialOfferID postgres - 4430; 2606 18182 FK CONSTRAINT sales store FK_Store_BusinessEntity_BusinessEntityID postgres - 4431; 2606 18187 FK CONSTRAINT sales store FK_Store_SalesPerson_SalesPersonID postgres - - ``` - -### Restoration Procedure - -To restore data to the target database, you can use the following commands: - -1. To restore data from a specific dump (identified by its **dumpId**), execute the following command: - ```shell - greenmask --config config.yml restore [dumpId] - ``` - Replace **[dumpId]** with the appropriate dump identifier. - -2. Alternatively, you can restore the latest available dump by using the reserved word **latest** like this: - ```shell - greenmask --config config.yml restore latest - ``` - -3. After the restoration process is complete, you can verify the restored data by running the following PostgreSQL - command: - ```shell - psql_t -xc 'select * from humanresources.employee limit 2;' - ``` - This command will display the first two rows of the "flights" table in the target database, showing the restored - data. - - ``` - -[ RECORD 1 ]----+------------------------------------- - businessentityid | 1 - nationalidnumber | 295847284 - loginid | adventure-works\ken0 - jobtitle | Chief Executive Officer - birthdate | 1968-12-18 - maritalstatus | S - gender | M - hiredate | 2009-01-14 - salariedflag | t - vacationhours | 99 - sickleavehours | 69 - currentflag | t - rowguid | f01251e5-96a3-448d-981e-0f99d789110d - modifieddate | 2014-06-30 00:00:00 - organizationnode | / - -[ RECORD 2 ]----+------------------------------------- - businessentityid | 2 - nationalidnumber | 245797967 - loginid | adventure-works\terri0 - jobtitle | Vice President of Engineering - birthdate | 1970-05-04 - maritalstatus | S - gender | F - hiredate | 2008-01-31 - salariedflag | t - vacationhours | 1 - sickleavehours | 20 - currentflag | t - rowguid | 45e8f437-670d-4409-93cb-f9424a40d6ee - modifieddate | 2014-06-30 00:00:00 - organizationnode | /1/ - - ``` - -### Deleting a Dump - -To remove a specific dump from the storage, use the **delete** command with the appropriate **dumpId**. -Here's how to do it: - -```shell -greenmask --config config.yml delete 1702489882319 -``` - -After executing this command, the specified dump will be deleted from the storage. - -To verify the changes, you can list the available dumps using the following command: - -The result - -```shell -greenmask --config config.yml list-dumps -``` - -The list displayed dumps will not include the deleted dump with the previously provided dumpId. - -``` -+----+------+----------+------+-----------------+----------+-------------+--------+ -| ID | DATE | DATABASE | SIZE | COMPRESSED SIZE | DURATION | TRANSFORMED | STATUS | -+----+------+----------+------+-----------------+----------+-------------+--------+ -+----+------+----------+------+-----------------+----------+-------------+--------+ -``` - -## Conclusion - -This is a straightforward example of using Greenmask. If you wish to explore more advanced transformation cases and -delve deeper into the documentation. - -Additionally, if you have any questions or require further assistance, don't hesitate to reach out via -[Discord](https://discord.gg/tAJegUKSTB), [Telegram](https://t.me/greenmask_community), or by -emailing us at [support@greenmask.io](mailto:support@greenmask.io). Our team is here to help and -provide guidance as needed. - From 3f2b70c1c02276fbebcfbb46eb19f79e09dc7e4e Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Sun, 12 May 2024 23:17:40 +0300 Subject: [PATCH 03/20] [docs] Removed old artifacts --- README.md | 4 ---- docs/architecture.md | 4 ---- 2 files changed, 8 deletions(-) diff --git a/README.md b/README.md index c1b9b224..1664e6f3 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,6 @@ Greenmask introduces the concept of **Storages**. various cloud-based storage solutions. * **directory** - This is the standard choice, representing the ordinary filesystem directory for local storage. -!!! note -If you have suggestions for additional storage options that would be valuable to implement, please feel free to -share your ideas. Greenmask aims to accommodate a wide range of storage preferences to suit diverse backup needs. - ## Restoration Process In the restoration process, Greenmask combines the capabilities of different tools: diff --git a/docs/architecture.md b/docs/architecture.md index c317cd65..b6f3f832 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -30,10 +30,6 @@ Greenmask introduces the concept of storages. * `s3` — this option supports any S3-like storage system, including AWS S3, which makes it versatile and adaptable to various cloud-based storage solutions. * `directory` — this is the standard choice, representing the ordinary filesystem directory for local storage. -!!! note - If you have suggestions for additional storage options that would be valuable to implement, feel free to - share your ideas with us. Greenmask aims to accommodate a wide range of storage preferences to suit diverse backup needs. - ## Restoration process In the restoration process, Greenmask combines the capabilities of different tools: From d5340cedfe8c41033328abe605d508f5abb92f8a Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Mon, 13 May 2024 21:25:31 +0300 Subject: [PATCH 04/20] docs: Edited documentation for NoiseDate transformer. Added info about global salt --- .../standard_transformers/noise_date.md | 74 ++++++++++++++++--- docs/configuration.md | 5 ++ 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/docs/built_in_transformers/standard_transformers/noise_date.md b/docs/built_in_transformers/standard_transformers/noise_date.md index 1307da43..bc5b14e6 100644 --- a/docs/built_in_transformers/standard_transformers/noise_date.md +++ b/docs/built_in_transformers/standard_transformers/noise_date.md @@ -2,22 +2,46 @@ Randomly add or subtract a duration within the provided `ratio` interval to the ## Parameters -| Name | Description | Default | Required | Supported DB types | -|----------|-------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------|------------------------------| -| column | The name of the column to be affected | | Yes | date, timestamp, timestamptz | -| ratio | The maximum random duration for noise. The value must be in PostgreSQL interval format, e. g. 1 year 2 mons 3 day — `04:05:06.07` | | Yes | - | -| truncate | Truncate the date to the specified part (`nano`, `second`, `minute`, `hour`, `day`, `month`, `year`). The truncate operation is not applied by default. | | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------|----------|------------------------------| +| column | The name of the column to be affected | | Yes | date, timestamp, timestamptz | +| min_ratio | The minimum random value for noise. The value must be in PostgreSQL interval format, e. g. `1 year 2 mons 3 day 04:05:06.07` | 5% from max_ration parameter | No | - | +| max_ratio | The maximum random value for noise. The value must be in PostgreSQL interval format, e. g. `1 year 2 mons 3 day 04:05:06.07` | | Yes | - | +| min | Min threshold date (and/or time) of value. The value has the same format as `column` parameter | | No | - | +| max | Max threshold date (and/or time) of value. The value has the same format as `column` parameter | | No | - | +| truncate | Truncate the date to the specified part (`nanosecond`, `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `month`, `year`). The truncate operation is not applied by default. | | No | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | + +## Dynamic parameters + +| Parameter | Supported types | +|-----------|------------------------------| +| min | date, timestamp, timestamptz | +| max | date, timestamp, timestamptz | ## Description -The `NoiseDate` transformer randomly generates duration within the specified `ratio` parameter and adds it to or +The `NoiseDate` transformer randomly generates duration between `min_ratio` and `max_ratio` parameter and adds it to or subtracts it from the original date value. The `ratio` parameter must be written in -the [PostgreSQL interval format](https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-INTERVAL-INPUT). You can also truncate the date up to a specified part by setting the `truncate` parameter. +the [PostgreSQL interval format](https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-INTERVAL-INPUT). +You can also truncate the resulted date up to a specified part by setting the `truncate` parameter. + +In case you have constraints on the date range, you can set the `min` and `max` parameters to specify the threshold +values. The values for `min` and `max` must have the same format as the `column` parameter. Parameters min and max +support dynamic mode. + +!!! info + + If the noised value exceeds the `max` threshold, the transformer will set the value to `max`. If the noised value + is lower than the `min` threshold, the transformer will set the value to `min`. + +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. ## Example: Adding noise to the modified date -In the following example, the original `timestamp` value of `modifieddate` will be noised up to `1 year 2 months 3 days 4 hours 5 -minutes 6 seconds and 7 milliseconds` with truncation up to the `nano` part. +In the following example, the original `timestamp` value of `modifieddate` will be noised up +to `1 year 2 months 3 days 4 hours 5 minutes 6 seconds and 7 milliseconds` with truncation up to the `month` part. ``` yaml title="NoiseDate transformer example" - schema: "humanresources" @@ -25,7 +49,33 @@ minutes 6 seconds and 7 milliseconds` with truncation up to the `nano` part. transformers: - name: "NoiseDate" params: - column: "modifieddate" - ratio: "1 year 2 mons 3 day 04:05:06.07" - truncate: "nano" + column: "hiredate" + max_ratio: "1 year 2 mons 3 day 04:05:06.07" + truncate: "month" + max: "2020-01-01 00:00:00" +``` + +## Example: Adding noise to the modified date with dynamic min parameter with hash engine + +In the following example, the original `timestamp` value of `hiredate` will be noised up +to `1 year 2 months 3 days 4 hours 5 minutes 6 seconds and 7 milliseconds` with truncation up to the `month` part. +The `max` threshold is set to `2020-01-01 00:00:00`, and the `min` threshold is set to the `birthdate` column. If the +`birthdate` column is `NULL`, the default value `1990-01-01` will be used. The hash engine is used for deterministic +generation - the same input will always produce the same output. + +``` yaml title="NoiseDate transformer example" +- schema: "humanresources" + name: "employee" + transformers: + - name: "NoiseDate" + params: + column: "hiredate" + max_ratio: "1 year 2 mons 3 day 04:05:06.07" + truncate: "month" + max: "2020-01-01 00:00:00" + engine: "hash" + dynamic_params: + min: + column: "birthdate" + default: "1990-01-01" ``` diff --git a/docs/configuration.md b/docs/configuration.md index 3319d869..69cd4695 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -309,6 +309,11 @@ log: level: debug ``` +### Global configuration variables + +* `GREENMASK_GLOBAL_SALT` - global salt value hex encoded with variadic length, used for the `hash` engine. For details + read [Transformation engines](built_in_transformers/transformation_engines.md) section. + ### Postgres connection variables Additionaly, there are some environment variables exposed by the `dump` and `restore` commands to facilitate the connection configuration with a Postgres database From 0914064cf0811ea5a631aba66fc8e78c4633b257 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Wed, 15 May 2024 14:20:15 +0300 Subject: [PATCH 05/20] docs: Fixed noise_date.md and refactored noise_float.md --- .../standard_transformers/noise_date.md | 6 +-- .../standard_transformers/noise_float.md | 38 +++++++++++++++---- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/docs/built_in_transformers/standard_transformers/noise_date.md b/docs/built_in_transformers/standard_transformers/noise_date.md index bc5b14e6..1c9c66d2 100644 --- a/docs/built_in_transformers/standard_transformers/noise_date.md +++ b/docs/built_in_transformers/standard_transformers/noise_date.md @@ -22,8 +22,8 @@ Randomly add or subtract a duration within the provided `ratio` interval to the ## Description The `NoiseDate` transformer randomly generates duration between `min_ratio` and `max_ratio` parameter and adds it to or -subtracts it from the original date value. The `ratio` parameter must be written in -the [PostgreSQL interval format](https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-INTERVAL-INPUT). +subtracts it from the original date value. The `min_ratio` or `max_ratio` parameters must be written in the +[PostgreSQL interval format](https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-INTERVAL-INPUT). You can also truncate the resulted date up to a specified part by setting the `truncate` parameter. In case you have constraints on the date range, you can set the `min` and `max` parameters to specify the threshold @@ -59,7 +59,7 @@ to `1 year 2 months 3 days 4 hours 5 minutes 6 seconds and 7 milliseconds` with In the following example, the original `timestamp` value of `hiredate` will be noised up to `1 year 2 months 3 days 4 hours 5 minutes 6 seconds and 7 milliseconds` with truncation up to the `month` part. -The `max` threshold is set to `2020-01-01 00:00:00`, and the `min` threshold is set to the `birthdate` column. If the +The `max` threshold is set to `2020-01-01 00:00:00`, and the `min` threshold is set to the `birthdate` column. If the `birthdate` column is `NULL`, the default value `1990-01-01` will be used. The hash engine is used for deterministic generation - the same input will always produce the same output. diff --git a/docs/built_in_transformers/standard_transformers/noise_float.md b/docs/built_in_transformers/standard_transformers/noise_float.md index eecef9ee..0159bf75 100644 --- a/docs/built_in_transformers/standard_transformers/noise_float.md +++ b/docs/built_in_transformers/standard_transformers/noise_float.md @@ -2,16 +2,35 @@ Add or subtract a random fraction to the original float value. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|----------------------------------------------------------------------------------------------------------|---------|----------|---------------------------------------------------| -| column | The name of the column to be affected | | Yes | float4 (real), float8 (double precision), numeric | -| ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | -| precision | The precision of the noised float value (number of digits after the decimal point) | `4` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|---------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | float4, float8 | +| precision | The precision of the noised float value (number of digits after the decimal point) | `4` | No | - | +| min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | +| max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | +| min | Min threshold of noised value | | No | - | +| max | Min threshold of noised value | | No | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | ## Description The `NoiseFloat` transformer multiplies the original float value by a provided random value that is not higher than -the `ratio` parameter and adds it to or subtracts it from the original value. Additionally, you can specify the number of decimal digits by using the `precision` parameter. +the `max_ratio` parameter and not less that `max_ratio` parameter and adds it to or subtracts it from the original +value. Additionally, you can specify the number of decimal digits by using the `precision` parameter. In case you have +constraints on the float range, you can set the `min` and `max` parameters to specify the threshold values. The values +for `min` and `max` must have the same format as the `column` parameter. Parameters min and max support dynamic mode. + +!!! info + + If the noised value exceeds the `max` threshold, the transformer will set the value to `max`. If the noised value + is lower than the `min` threshold, the transformer will set the value to `min`. + +## Dynamic parameters + +| Parameter | Supported types | +|-----------|----------------------------------| +| min | float4, float8, int2, int4, int8 | +| max | float4, float8, int2, int4, int8 | ## Example: Adding noise to the purchase price @@ -23,7 +42,10 @@ In this example, the original value of `standardprice` will be noised up to `50% transformers: - name: "NoiseFloat" params: - column: "standardprice" - ratio: 0.5 + column: "lastreceiptcost" + max_ratio: 0.15 precision: 2 + dynamic_params: + min: + column: "standardprice" ``` From 6d53c2c3b7af8f8c8fc9a38f58e84a67ff920bd5 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Wed, 15 May 2024 22:47:08 +0300 Subject: [PATCH 06/20] doc: Reviewed documentation for NoiseInt and NoiseFloat transformers --- .../standard_transformers/noise_float.md | 28 +++++++----- .../standard_transformers/noise_int.md | 43 +++++++++++++++---- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/docs/built_in_transformers/standard_transformers/noise_float.md b/docs/built_in_transformers/standard_transformers/noise_float.md index 0159bf75..c30ffc71 100644 --- a/docs/built_in_transformers/standard_transformers/noise_float.md +++ b/docs/built_in_transformers/standard_transformers/noise_float.md @@ -9,28 +9,34 @@ Add or subtract a random fraction to the original float value. | min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | | max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | | min | Min threshold of noised value | | No | - | -| max | Min threshold of noised value | | No | - | +| max | Max threshold of noised value | | No | - | | engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +## Dynamic parameters + +| Parameter | Supported types | +|-----------|----------------------------------| +| min | float4, float8, int2, int4, int8 | +| max | float4, float8, int2, int4, int8 | + ## Description -The `NoiseFloat` transformer multiplies the original float value by a provided random value that is not higher than +The `NoiseFloat` transformer multiplies the original float value by randomly generated value that is not higher than the `max_ratio` parameter and not less that `max_ratio` parameter and adds it to or subtracts it from the original -value. Additionally, you can specify the number of decimal digits by using the `precision` parameter. In case you have -constraints on the float range, you can set the `min` and `max` parameters to specify the threshold values. The values -for `min` and `max` must have the same format as the `column` parameter. Parameters min and max support dynamic mode. +value. Additionally, you can specify the number of decimal digits by using the `precision` parameter. + +In case you have constraints on the float range, you can set the `min` and `max` parameters to specify the threshold +values. The values for `min` and `max` must have the same format as the `column` parameter. Parameters min and max +support dynamic mode. Engine parameter allows you to choose between random and hash engines for generating values. Read +more about the engines !!! info If the noised value exceeds the `max` threshold, the transformer will set the value to `max`. If the noised value is lower than the `min` threshold, the transformer will set the value to `min`. -## Dynamic parameters - -| Parameter | Supported types | -|-----------|----------------------------------| -| min | float4, float8, int2, int4, int8 | -| max | float4, float8, int2, int4, int8 | +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. ## Example: Adding noise to the purchase price diff --git a/docs/built_in_transformers/standard_transformers/noise_int.md b/docs/built_in_transformers/standard_transformers/noise_int.md index 759e633f..a366e7cf 100644 --- a/docs/built_in_transformers/standard_transformers/noise_int.md +++ b/docs/built_in_transformers/standard_transformers/noise_int.md @@ -2,19 +2,44 @@ Add or subtract a random fraction to the original integer value. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|--------|------------------------------------------------------------------|---------|----------|--------------------| -| column | The name of the column to be affected | | Yes | int2, int4, int8 | -| ratio | The maximum random percentage for noise, from `0` to `1` | | Yes | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|---------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | int2, int4, int8 | +| min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | +| max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | +| min | Min threshold of noised value | | No | - | +| max | Min threshold of noised value | | No | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | + +## Dynamic parameters + +| Parameter | Supported types | +|-----------|------------------| +| min | int2, int4, int8 | +| max | int2, int4, int8 | ## Description -The `NoiseInt` transformer multiplies the original integer value by a provided random value that is not higher than the -`ratio` parameter and adds it to or subtracts it from the original value. +The `NoiseInt` transformer multiplies the original integer value by randomly generated value that is not higher than +the `max_ratio` parameter and not less that `max_ratio` parameter and adds it to or subtracts it from the original +value. + +In case you have constraints on the integer range, you can set the `min` and `max` parameters to specify the +threshold values. The values for `min` and `max` must have the same format as the `column` parameter. Parameters min and +max support dynamic mode. + +!!! info + + If the noised value exceeds the `max` threshold, the transformer will set the value to `max`. If the noised value + is lower than the `min` threshold, the transformer will set the value to `min`. + +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. ## Example: Noise vacation hours of an employee -In the following example, the original value of `vacationhours` will be noised up to 40%. +In the following example, the original value of `vacationhours` will be noised up to 40%. The transformer will set the +value to `10` if the noised value is lower than `10` and to `1000` if the noised value exceeds `1000`. ``` yaml title="NoiseInt transformer example" - schema: "humanresources" @@ -23,5 +48,7 @@ In the following example, the original value of `vacationhours` will be noised u - name: "NoiseInt" params: column: "vacationhours" - ratio: 0.4 + max_ratio: 0.4 + min: 10 + max: 1000 ``` From 1def11fbba81bc4638c9bbd6a9531ff567fa0351 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Wed, 15 May 2024 22:54:50 +0300 Subject: [PATCH 07/20] doc: Reviewed RandomBool transformer --- .../standard_transformers/random_bool.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/built_in_transformers/standard_transformers/random_bool.md b/docs/built_in_transformers/standard_transformers/random_bool.md index 89f51f07..ee19b08b 100644 --- a/docs/built_in_transformers/standard_transformers/random_bool.md +++ b/docs/built_in_transformers/standard_transformers/random_bool.md @@ -2,15 +2,18 @@ Generate random boolean values. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|------------------------------------------------------------------------------|---------|----------|--------------------| -| column | The name of the column to be affected | | Yes | bool | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|-------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | bool | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | ## Description The `RandomBool` transformer generates a random boolean value. The behaviour for NULL values can be -configured using the `keep_null` parameter. +configured using the `keep_null` parameter. The `engine` parameter allows you to choose between random and hash engines +for generating values. Read more about the engines in the [Transformation engines](../transformation_engines.md) +section. ## Example: Generate a random boolean for a column From e0ff104fc59260b49845a0da42648b55e8c4f71f Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Wed, 15 May 2024 22:57:14 +0300 Subject: [PATCH 08/20] doc: Reviewed RandomChoice transformer --- .../standard_transformers/random_choice.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/built_in_transformers/standard_transformers/random_choice.md b/docs/built_in_transformers/standard_transformers/random_choice.md index 19d5f3fb..7e31d21e 100644 --- a/docs/built_in_transformers/standard_transformers/random_choice.md +++ b/docs/built_in_transformers/standard_transformers/random_choice.md @@ -2,17 +2,22 @@ Replace values randomly chosen from a provided list. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|-----------------------------------------------------------------------------------------------------------------------|---------|----------|--------------------| -| column | The name of the column to be affected | | Yes | any | -| values | A list of values in any format. The string with value `\N` is considered NULL. | | Yes | - | -| validate | Performs a decoding procedure via the PostgreSQL driver using the column type to ensure that values have correct type | `true` | No | | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | | +| Name | Description | Default | Required | Supported DB types | +|-----------|-----------------------------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | any | +| values | A list of values in any format. The string with value `\N` is considered NULL. | | Yes | - | +| validate | Performs a decoding procedure via the PostgreSQL driver using the column type to ensure that values have correct type | `true` | No | | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | ## Description The `RandomChoice` transformer replaces one randomly chosen value from the list provided in the `values` parameter. You -can use the `validate` parameter to ensure that values are correct before applying the transformation. The behaviour for NULL values can be configured using the `keep_null` parameter. +can use the `validate` parameter to ensure that values are correct before applying the transformation. The behaviour for +NULL values can be configured using the `keep_null` parameter. + +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. ## Example: Choosing randomly from provided dates From febd11407b4659a1021882672be7cd623d907e72 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Wed, 15 May 2024 22:59:10 +0300 Subject: [PATCH 09/20] doc: Reviewed RandomUuid transformer --- .../standard_transformers/random_uuid.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/built_in_transformers/standard_transformers/random_uuid.md b/docs/built_in_transformers/standard_transformers/random_uuid.md index f3baa8d2..9d2bb9ae 100644 --- a/docs/built_in_transformers/standard_transformers/random_uuid.md +++ b/docs/built_in_transformers/standard_transformers/random_uuid.md @@ -2,14 +2,19 @@ Generate random unique user ID using version 4. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|------------------------------------------------------------------------------|---------|----------|---------------------| -| column | The name of the column to be affected | | Yes | text, varchar, uuid | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|-------------------------------------------------------------------------------------------------|----------|----------|---------------------| +| column | The name of the column to be affected | | Yes | text, varchar, uuid | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | ## Description -The `RandomUuid` transformer generates a random UUID. The behaviour for NULL values can be configured using the `keep_null` parameter. +The `RandomUuid` transformer generates a random UUID. The behaviour for NULL values can be configured using +the `keep_null` parameter. + +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. ## Example: Updating the `rowguid` column @@ -25,7 +30,7 @@ The following example replaces original UUID values of the `rowguid` column to r keep_null: false ``` -```bash title="Expected result" +```title="Expected result" | column name | original value | transformed | |-------------|--------------------------------------|--------------------------------------| From 7cf838f983a51d1f89c8bf28feba16b608e04821 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Wed, 15 May 2024 23:03:12 +0300 Subject: [PATCH 10/20] doc: Reviewed RandomString transformer --- .../standard_transformers/random_string.md | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/built_in_transformers/standard_transformers/random_string.md b/docs/built_in_transformers/standard_transformers/random_string.md index 8384c7e3..8578fbf7 100644 --- a/docs/built_in_transformers/standard_transformers/random_string.md +++ b/docs/built_in_transformers/standard_transformers/random_string.md @@ -2,22 +2,28 @@ Generate a random string using the provided characters within the specified leng ## Parameters -| Name | Description | Default | Required | Supported DB types | -|------------|------------------------------------------------------------------------------|--------------------------------------------------------|----------|--------------------| -| column | The name of the column to be affected | | Yes | text, varchar | -| min_length | The minimum length of the generated string | | Yes | - | -| max_length | The maximum length of the generated string | | Yes | - | -| symbols | The range of characters that can be used in the random string | `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` | No | - | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| Name | Description | Default | Required | Supported DB types | +|------------|-------------------------------------------------------------------------------------------------|--------------------------------------------------------|----------|--------------------| +| column | The name of the column to be affected | | Yes | text, varchar | +| min_length | The minimum length of the generated string | | Yes | - | +| max_length | The maximum length of the generated string | | Yes | - | +| symbols | The range of characters that can be used in the random string | `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` | No | - | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | ## Description The `RandomString` transformer generates a random string with a length between `min_length` and `max_length` using the -characters specified in the symbols string as the possible set of characters. The behaviour for NULL values can be configured using the `keep_null` parameter. +characters specified in the symbols string as the possible set of characters. The behaviour for NULL values can be +configured using the `keep_null` parameter. + +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. ## Example: Generate a random string for `accountnumber` -In the following example, a random string is generated for the `accountnumber` column with a length range from `9` to `12`. The +In the following example, a random string is generated for the `accountnumber` column with a length range from `9` +to `12`. The character set used for generation includes `1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ`. ``` yaml title="RandomString transformer example" From 5d13609fd8904a471758ca4fd7db41af583d5f2b Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Wed, 15 May 2024 23:17:58 +0300 Subject: [PATCH 11/20] doc: Added NoiseNumeric transformer documentation --- .../standard_transformers/index.md | 3 +- .../standard_transformers/noise_float.md | 8 ++- .../standard_transformers/noise_numeric.md | 63 +++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 docs/built_in_transformers/standard_transformers/noise_numeric.md diff --git a/docs/built_in_transformers/standard_transformers/index.md b/docs/built_in_transformers/standard_transformers/index.md index 29882003..c5d29cca 100644 --- a/docs/built_in_transformers/standard_transformers/index.md +++ b/docs/built_in_transformers/standard_transformers/index.md @@ -7,7 +7,8 @@ Standard transformers are ready-to-use methods that require no customization and 1. [Hash](dict.md) — generates a hash of the text value. 1. [Masking](masking.md) — masks a value using one of the masking behaviors depending on your domain. 1. [NoiseDate](noise_date.md) — randomly adds or subtracts a duration within the provided ratio interval to the original date value. -1. [NoiseFloat](noise_float.md) — adds or subtracts a random fraction to the original float value. +1. [NoiseFloat](noise_float.md) — adds or subtracts a random fraction to the original float value.terval to the original date value. +1. [NoiseNumeric](noise_numeric.md) — adds or subtracts a random fraction to the original numeric value. 1. [NoiseInt](noise_int.md) — adds or subtracts a random fraction to the original integer value. 1. [RandomBool](random_bool.md) — generates random boolean values. 1. [RandomChoice](random_choice.md) — replaces values randomly chosen from a provided list. diff --git a/docs/built_in_transformers/standard_transformers/noise_float.md b/docs/built_in_transformers/standard_transformers/noise_float.md index c30ffc71..8c3ff1f0 100644 --- a/docs/built_in_transformers/standard_transformers/noise_float.md +++ b/docs/built_in_transformers/standard_transformers/noise_float.md @@ -46,7 +46,10 @@ In this example, the original value of `standardprice` will be noised up to `50% - schema: "purchasing" name: "productvendor" transformers: - - name: "NoiseFloat" + - name: "NoiseFloat" + columns_type_override: # (1) + lastreceiptcost: "numeric" + standardprice: "numeric" params: column: "lastreceiptcost" max_ratio: 0.15 @@ -55,3 +58,6 @@ In this example, the original value of `standardprice` will be noised up to `50% min: column: "standardprice" ``` + +1. The type overrides perfomed for example because the playground database does not contains any tables with float + columns. diff --git a/docs/built_in_transformers/standard_transformers/noise_numeric.md b/docs/built_in_transformers/standard_transformers/noise_numeric.md new file mode 100644 index 00000000..19ec02ba --- /dev/null +++ b/docs/built_in_transformers/standard_transformers/noise_numeric.md @@ -0,0 +1,63 @@ +Add or subtract a random fraction to the original numeric value. + +## Parameters + +| Name | Description | Default | Required | Supported DB types | +|-----------|---------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | numeric, decimal | +| precision | The precision of the noised float value (number of digits after the decimal point) | `4` | No | - | +| min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | +| max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | +| min | Min threshold of noised value | | No | - | +| max | Max threshold of noised value | | No | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | + +## Dynamic parameters + +| Parameter | Supported types | +|-----------|----------------------------------------------------| +| min | numeric, decimal, float4, float8, int2, int4, int8 | +| max | numeric, decimal, float4, float8, int2, int4, int8 | + +## Description + +The `NoiseNumeric` transformer multiplies the original numeric (or decimal) value by randomly generated value that is +not higher than the `max_ratio` parameter and not less that `max_ratio` parameter and adds it to or subtracts it from +the original value. Additionally, you can specify the number of decimal digits by using the `precision` parameter. + +In case you have constraints on the numeric range, you can set the `min` and `max` parameters to specify the threshold +values. The values for `min` and `max` must have the same format as the `column` parameter. Parameters min and max +support dynamic mode. Engine parameter allows you to choose between random and hash engines for generating values. Read +more about the engines + +!!! info + + If the noised value exceeds the `max` threshold, the transformer will set the value to `max`. If the noised value + is lower than the `min` threshold, the transformer will set the value to `min`. + +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. + +!!! warning + + Greenmask cannot parse the `numeric` type sitteng. For instance `NUMERIC(10, 2)`. You should set `min` and `max` treshholds + manually as well as allowed `precision`. This behaviour will be changed in the later versions. Grenmask will be able + to determine the precision and scale of the column and set the min and max treshholds automatically if were not set. + +## Example: Adding noise to the purchase price + +In this example, the original value of `standardprice` will be noised up to `50%` and rounded up to `2` decimals. + +``` yaml title="NoiseNumeric transformer example" +- schema: "purchasing" + name: "productvendor" + transformers: + - name: "NoiseNumeric" + params: + column: "lastreceiptcost" + max_ratio: 0.15 + precision: 2 + dynamic_params: + min: + column: "standardprice" +``` diff --git a/mkdocs.yml b/mkdocs.yml index f3069b84..b730c651 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -63,6 +63,7 @@ nav: - Masking: built_in_transformers/standard_transformers/masking.md - NoiseDate: built_in_transformers/standard_transformers/noise_date.md - NoiseFloat: built_in_transformers/standard_transformers/noise_float.md + - NoiseNumeric: built_in_transformers/standard_transformers/noise_numeric.md - NoiseInt: built_in_transformers/standard_transformers/noise_int.md - RandomBool: built_in_transformers/standard_transformers/random_bool.md - RandomChoice: built_in_transformers/standard_transformers/random_choice.md From 8b8a0a711def5e68d2949945bf5715065c00f075 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Thu, 16 May 2024 11:10:24 +0300 Subject: [PATCH 12/20] fix: renamed precision parameter to decimal in docs --- config.yml.example | 2 +- .../custom_functions/core_functions.md | 16 ++++++++-------- .../standard_transformers/noise_float.md | 6 +++--- .../standard_transformers/random_float.md | 17 +++++++++-------- docs/commands.md | 4 ++-- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/config.yml.example b/config.yml.example index 5540208e..d8a43327 100644 --- a/config.yml.example +++ b/config.yml.example @@ -155,7 +155,7 @@ dump: params: ratio: 0.1 column: "test_float" - precision: 2 + decimal: 2 restore: pg_restore_options: diff --git a/docs/built_in_transformers/advanced_transformers/custom_functions/core_functions.md b/docs/built_in_transformers/advanced_transformers/custom_functions/core_functions.md index e470d482..221ccdd7 100644 --- a/docs/built_in_transformers/advanced_transformers/custom_functions/core_functions.md +++ b/docs/built_in_transformers/advanced_transformers/custom_functions/core_functions.md @@ -113,16 +113,16 @@ Adds or subtracts a random duration in the provided `interval` to or from the or ### noiseFloat -Adds or subtracts a random fraction to or from the original float value. Multiplies the original float value by a provided random value that is not higher than the `ratio` parameter and adds it to the original value with the option to specify the precision via the `precision` parameter. +Adds or subtracts a random fraction to or from the original float value. Multiplies the original float value by a provided random value that is not higher than the `ratio` parameter and adds it to the original value with the option to specify the decimal via the `decimal` parameter. === "Signature" - `noiseFloat(ratio float, precision int, value float) (res float64, err error)` + `noiseFloat(ratio float, decimal int, value float) (res float64, err error)` === "Parameters" * `ratio` — the maximum multiplier value in the interval (0:1). The value will be randomly generated up to `ratio`, multiplied by the original value, and the result will be added to the original value. - * `precision` — the precision of the resulted value + * `decimal` — the decimal of the resulted value * `value` — the original value === "Return values" @@ -176,13 +176,13 @@ Generates a random float value within the provided interval. === "Signature" - `randomFloat(min any, max any, precision int) (res float, err error)` + `randomFloat(min any, max any, decimal int) (res float, err error)` === "Parameters" * `min` — the minimum random value threshold * `max` — the maximum random value threshold - * `precision` — the precision of the resulted value + * `decimal` — the decimal of the resulted value === "Return values" @@ -229,15 +229,15 @@ Generates a random string using the provided characters within the specified len ### roundFloat -Rounds a float value up to provided precision. +Rounds a float value up to provided decimal. === "Signature" - `roundFloat(precision int, original float) (res float, err error)` + `roundFloat(decimal int, original float) (res float, err error)` === "Parameters" - * `precision` — the precision of the value + * `decimal` — the decimal of the value * `original` — the original float value === "Return values" diff --git a/docs/built_in_transformers/standard_transformers/noise_float.md b/docs/built_in_transformers/standard_transformers/noise_float.md index 8c3ff1f0..18fa2c10 100644 --- a/docs/built_in_transformers/standard_transformers/noise_float.md +++ b/docs/built_in_transformers/standard_transformers/noise_float.md @@ -5,7 +5,7 @@ Add or subtract a random fraction to the original float value. | Name | Description | Default | Required | Supported DB types | |-----------|---------------------------------------------------------------------------------------------------|----------|----------|--------------------| | column | The name of the column to be affected | | Yes | float4, float8 | -| precision | The precision of the noised float value (number of digits after the decimal point) | `4` | No | - | +| decimal | The decimal of the noised float value (number of digits after the decimal point) | `4` | No | - | | min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | | max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | | min | Min threshold of noised value | | No | - | @@ -23,7 +23,7 @@ Add or subtract a random fraction to the original float value. The `NoiseFloat` transformer multiplies the original float value by randomly generated value that is not higher than the `max_ratio` parameter and not less that `max_ratio` parameter and adds it to or subtracts it from the original -value. Additionally, you can specify the number of decimal digits by using the `precision` parameter. +value. Additionally, you can specify the number of decimal digits by using the `decimal` parameter. In case you have constraints on the float range, you can set the `min` and `max` parameters to specify the threshold values. The values for `min` and `max` must have the same format as the `column` parameter. Parameters min and max @@ -53,7 +53,7 @@ In this example, the original value of `standardprice` will be noised up to `50% params: column: "lastreceiptcost" max_ratio: 0.15 - precision: 2 + decimal: 2 dynamic_params: min: column: "standardprice" diff --git a/docs/built_in_transformers/standard_transformers/random_float.md b/docs/built_in_transformers/standard_transformers/random_float.md index c0db5b70..8dc7d4e5 100644 --- a/docs/built_in_transformers/standard_transformers/random_float.md +++ b/docs/built_in_transformers/standard_transformers/random_float.md @@ -2,23 +2,24 @@ Generate a random float within the provided interval. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|----------------------------------------------------------------------------------------|---------|----------|---------------------------------------------------| -| column | The name of the column to be affected | | Yes | float4 (real), float8 (double precision), numeric | +| Name | Description | Default | Required | Supported DB types | +|-----------|-----------------------------------------------------------------------------------------|---------|----------|---------------------------------------------------| +| column | The name of the column to be affected | | Yes | float4 (real), float8 (double precision), numeric | | min | The minimum threshold for the random value. The value range depends on the column type. | | Yes | - | | max | The maximum threshold for the random value. The value range depends on the column type. | | Yes | - | -| precision | The precision of the random float value (number of digits after the decimal point) | `4` | No | - | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| decimal | The decimal of the random float value (number of digits after the decimal point) | `4` | No | - | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | ## Description The `RandomFloat` transformer generates a random float value within the provided interval, starting from `min` to -`max`, with the option to specify the number of decimal digits by using the `precision` parameter. The behaviour for NULL values can be configured using the `keep_null` parameter. +`max`, with the option to specify the number of decimal digits by using the `decimal` parameter. The behaviour for +NULL values can be configured using the `keep_null` parameter. ## Example: Generate random price In this example, the `RandomFloat` transformer generates random prices in the range from `0.1` to `7000` while -maintaining a precision of up to 2 digits. +maintaining a decimal of up to 2 digits. ``` yaml title="RandomFloat transformer example" - schema: "sales" @@ -29,7 +30,7 @@ maintaining a precision of up to 2 digits. column: "unitprice" min: 0.1 max: 7000 - precision: 2 + decimal: 2 ``` ```bash title="Expected result" diff --git a/docs/commands.md b/docs/commands.md index da36df33..e16e6ab7 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -522,8 +522,8 @@ their possible attributes. Below are the key parameters for each transformer: "default_value": "MC4x" }, { - "name": "precision", - "description": "precision of noised float value (number of digits after coma)", + "name": "decimal", + "description": "decimal of noised float value (number of digits after coma)", "required": false, "is_column": false, "is_column_container": false, From 48250d3cb5ae56a8f9a8d4bfb953ce34b650e582 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Thu, 16 May 2024 18:33:52 +0300 Subject: [PATCH 13/20] doc: Revised doc for RandomInt transformer --- config.yml.example | 172 +++++++----------- .../standard_transformers/random_int.md | 50 ++++- mkdocs.yml | 1 + 3 files changed, 105 insertions(+), 118 deletions(-) diff --git a/config.yml.example b/config.yml.example index d8a43327..aa689c53 100644 --- a/config.yml.example +++ b/config.yml.example @@ -25,137 +25,91 @@ dump: load-via-partition-root: true transformation: - - schema: "bookings" - name: "flights" - query: "select * from bookings.flights limit 100" - columns_type_override: - post_code: "int4" + - schema: "public" + name: "account" transformers: - - name: "RandomDate" - params: - min: "2023-01-01 00:00:00.0+03" - max: "2023-01-02 00:00:00.0+03" - column: "scheduled_departure" - - - name: "NoiseDate" + - name: "RandomInt" params: - ratio: "1 day" - column: "scheduled_arrival" + column: "id" + engine: hash + min: 1 + max: 2147483647 - - name: "RegexpReplace" + - name: "RandomChoice" params: - column: "departure_airport" - regexp: "DME" - replace: "SVO" + column: "gender" + values: + - "M" + - "F" - - name: "RegexpReplace" + - name: "RandomPerson" params: - column: "status" - regexp: "On Time" - replace: "Delayed" + columns: + - name: "first_name" + template: "{{ .FirstName }}" + - name: "last_name" + template: "{{ .LastName }}" + dynamic_params: + gender: + column: gender + + - name: "Email" + params: + column: "email" + engine: "hash" + keep_original_domain: true + keep_null: false + local_part_template: "{{ first_name | lower }}.{{ last_name | lower }}" - name: "RandomDate" params: - column: "actual_departure" - min: "2023-01-03 01:00:00.0+03" - max: "2023-01-04 00:00:00.0+03" + column: "birth_date" + min: '{{ now | tsModify "-30 years" | .EncodeValue }}' # 1994 + max: '{{ now | tsModify "-18 years" | .EncodeValue }}' # 2006 - name: "RandomDate" params: - column: "actual_arrival" - min: "2023-01-04 01:00:00.0+03" - max: "2023-01-05 00:00:00.0+03" + column: "created_at" + max: "{{ now | .EncodeValue }}" + truncate: "day" + dynamic_params: + min: + column: "birth_date" + template: '{{ .GetValue | tsModify "18 years" | .EncodeValue }}' - - name: "RandomInt" - params: - column: "post_code" - min: "11" - max: "99" - - - name: "Replace" - params: - column: "post_code" - value: "54321" + - schema: "public" + name: "orders" + transformers: - - name: "TwoDatesGen" + - name: "RandomInt" params: - column_a: "scheduled_arrival" - column_b: "actual_arrival" + column: "account_id" + engine: hash + min: 1 + max: 2147483647 - - name: "TestTransformer" + - name: "NoiseNumeric" params: - column: "actual_arrival" + column: "total_price" + decimal: 2 + min_ratio: 0.1 + max_ratio: 0.9 - - name: "Cmd" - params: - executable: "cmd_test.sh" - driver: - name: "json" - params: - format: "bytes" - timeout: "60s" - validate_output: true - expected_exit_code: -1 - skip_on_behaviour: "any" - columns: - - name: "actual_arrival" - skip_original_data: true - skip_on_null_input: true - - name: "scheduled_arrival" - skip_original_data: true -# - - name: "TestTransformer" + - name: "NoiseDate" params: - column: "scheduled_arrival" + column: "created_at" + max_ratio: "6 day" + min_ratio: "1 day" + truncate: "day" - - schema: "bookings" - name: "measurement" - apply_for_inherited: True - transformers: - name: "RandomDate" params: - column: "logdate" - min: "2023-01-03" - max: "2023-01-30" - - - name: "TemplateRecord" - params: - validate: false - columns: - - "scheduled_departure" - template: > - {{- $val := .GetValue "scheduled_departure" -}} - {{- if isNull $val -}} - {{ now | dateModify "24h" | .SetValue "scheduled_departure" }} - {{ else }} - {{ now | dateModify "48h" | .SetValue "scheduled_departure" }} - {{ end }} - - - - schema: "bookings" - name: "aircrafts_data" - transformers: - - name: "Json" - params: - column: "model" - operations: - - operation: "set" - path: "en" - value: "Boeing 777-300-2023" - - operation: "set" - path: "crewSize" - value: 10 - - - name: "NoiseInt" - params: - ratio: 0.9 - column: "range" - - - name: "NoiseFloat" - params: - ratio: 0.1 - column: "test_float" - decimal: 2 + column: "paid_at" + max: '{{ now | .EncodeValue }}' + truncate: "day" + dynamic_params: + min: + column: "created_at" restore: pg_restore_options: diff --git a/docs/built_in_transformers/standard_transformers/random_int.md b/docs/built_in_transformers/standard_transformers/random_int.md index 71db66fb..9c86f55f 100644 --- a/docs/built_in_transformers/standard_transformers/random_int.md +++ b/docs/built_in_transformers/standard_transformers/random_int.md @@ -2,23 +2,36 @@ Generate a random integer within the provided interval. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|----------------------------------------------------------------------------------------|---------|----------|-----------------------------------------------------| -| column | The name of the column to be affected | | Yes | int2 (smallint), int4 (int), int8 (bigint), numeric | -| min | The minimum threshold for the random value. The value range depends on the column type. | | Yes | - | -| max | The maximum threshold for the random value. The value range depends on the column type. | | Yes | - | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|-------------------------------------------------------------------------------------------------|----------|----------|-----------------------------------------------------| +| column | The name of the column to be affected | | Yes | int2 (smallint), int4 (int), int8 (bigint), numeric | +| min | The minimum threshold for the random value. The value range depends on the column type. | | Yes | - | +| max | The maximum threshold for the random value. The value range depends on the column type. | | Yes | - | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | + +## Dynamic parameters + +| Parameter | Supported types | +|-----------|------------------| +| min | int2, int4, int8 | +| max | int2, int4, int8 | ## Description -The `RandomInt` transformer generates a random integer within the specified `min` and `max` thresholds. The behaviour for NULL values can be configured using the `keep_null` parameter. +The `RandomInt` transformer generates a random integer within the specified `min` and `max` thresholds. The behaviour +for NULL values can be configured using the `keep_null` parameter. + +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. ## Example: Generate random item quantity -In the following example, the `RandomInt` transformer generates a random value in the range from `1` to `30` and assigns it to +In the following example, the `RandomInt` transformer generates a random value in the range from `1` to `30` and assigns +it to the `orderqty` column. -``` yaml title="RandomInt transformer example" +``` yaml title="generate random orderqty in the range from 1 to 30" - schema: "sales" name: "salesorderdetail" transformers: @@ -35,3 +48,22 @@ the `orderqty` column. |-------------|----------------|-------------| | orderqty | 1 | 8 | ``` + +## Example: Generate random sick leave hours based on vacation hours + +In the following example, the `RandomInt` transformer generates a random value in the range from `1` to the value of the +`vacationhours` column and assigns it to the `sickleavehours` column. This configuration allows for the simulation of +sick leave hours based on the number of vacation hours. + +``` yaml title="RandomInt transformer example" +- schema: "humanresources" + name: "employee" + transformers: + - name: "RandomInt" + params: + column: "sickleavehours" + max: 100 + dynamic_params: + min: + column: "vacationhours" +``` diff --git a/mkdocs.yml b/mkdocs.yml index 27775fe8..e51f24ec 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -88,6 +88,7 @@ nav: - RandomDomainName: built_in_transformers/standard_transformers/random_domain_name.md - RandomPerson: built_in_transformers/standard_transformers/random_person.md - RandomURL: built_in_transformers/standard_transformers/random_url.md + - RandomMac: built_in_transformers/standard_transformers/random_mac.md - RandomIP: built_in_transformers/standard_transformers/random_ip.md - RandomWord: built_in_transformers/standard_transformers/random_word.md - RandomSentence: built_in_transformers/standard_transformers/random_sentence.md From a085b15bd122d7999efffd7c0168de08745fe21a Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Thu, 16 May 2024 20:14:59 +0300 Subject: [PATCH 14/20] doc: Added transformation result in some transformers doc --- .../standard_transformers/dict.md | 37 ++++++-- .../standard_transformers/index.md | 1 - .../standard_transformers/noise_date.md | 12 +++ .../standard_transformers/noise_float.md | 20 ++++- .../standard_transformers/noise_int.md | 11 +++ .../standard_transformers/noise_numeric.md | 22 +++-- .../standard_transformers/random_bool.md | 11 +++ .../standard_transformers/random_choice.md | 33 ++++--- .../standard_transformers/random_date.md | 88 ++++++++++++++++--- .../standard_transformers/random_int.md | 41 ++++++--- .../standard_transformers/random_ip.md | 56 ++++++++---- .../standard_transformers/random_mac.md | 57 +++++++----- .../random_mac_address.md | 29 ------ .../standard_transformers/random_person.md | 76 +++++++++++----- .../standard_transformers/random_string.md | 16 ++-- .../standard_transformers/random_uuid.md | 16 ++-- mkdocs.yml | 1 - 17 files changed, 369 insertions(+), 158 deletions(-) delete mode 100644 docs/built_in_transformers/standard_transformers/random_mac_address.md diff --git a/docs/built_in_transformers/standard_transformers/dict.md b/docs/built_in_transformers/standard_transformers/dict.md index 35c18dbd..c21c589e 100644 --- a/docs/built_in_transformers/standard_transformers/dict.md +++ b/docs/built_in_transformers/standard_transformers/dict.md @@ -4,23 +4,30 @@ Replace values matched by dictionary keys. | Name | Description | Default | Required | Supported DB types | |------------------|----------------------------------------------------------------------------------------------------------------------------------------|---------|----------|--------------------| -| column | The name of the column to be affected | | Yes | any | -| values | Value replace mapping as in: `{"string": "string"}`. The string with value `"\N"` is considered NULL. | | No | - | -| default | Shown if no value has been matched with dict. The string with value `"\N"` is considered NULL. By default is empty. | | No | - | -| fail_not_matched | When no value is matched with the dict, fails the replacement process if set to `true`, or keeps the current value, if set to `false`. | `true` | No | - | -| validate | Performs the encode-decode procedure using column type to ensure that values have correct type | `true` | No | - | +| column | The name of the column to be affected | | Yes | any | +| values | Value replace mapping as in: `{"string": "string"}`. The string with value `"\N"` is considered NULL. | | No | - | +| default | Shown if no value has been matched with dict. The string with value `"\N"` is considered NULL. By default is empty. | | No | - | +| fail_not_matched | When no value is matched with the dict, fails the replacement process if set to `true`, or keeps the current value, if set to `false`. | `true` | No | - | +| validate | Performs the encode-decode procedure using column type to ensure that values have correct type | `true` | No | - | ## Description -The `Dict` transformer uses a user-provided key-value dictionary to replace values based on matches specified in the `values` parameter mapping. These provided values must align with the PostgreSQL type format. To validate the values format before application, you can utilize the `validate` parameter, triggering a decoding procedure via the PostgreSQL driver. +The `Dict` transformer uses a user-provided key-value dictionary to replace values based on matches specified in +the `values` parameter mapping. These provided values must align with the PostgreSQL type format. To validate the values +format before application, you can utilize the `validate` parameter, triggering a decoding procedure via the PostgreSQL +driver. -If there are no matches by key, an error will be raised according to a default `fail_not_matched: true` parameter. You can change this behaviour by providing the `default` parameter, value from which will be shown in case of a missing match. +If there are no matches by key, an error will be raised according to a default `fail_not_matched: true` parameter. You +can change this behaviour by providing the `default` parameter, value from which will be shown in case of a missing +match. -In certain cases where the driver type does not support the validation operation, an error may occur. For setting or matching a NULL value, use a string with the `\N` sequence. +In certain cases where the driver type does not support the validation operation, an error may occur. For setting or +matching a NULL value, use a string with the `\N` sequence. ## Example: Replace marital status -The following example replaces marital status from `S` to `M` or from `M` to `S` and raises an error if there is no match: +The following example replaces marital status from `S` to `M` or from `M` to `S` and raises an error if there is no +match: ``` yaml title="Dict transformer example" - schema: "humanresources" @@ -35,3 +42,15 @@ The following example replaces marital status from `S` to `M` or from `M` to `S` validate: true fail_not_matched: true ``` + +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
maritalstatusSM
+ diff --git a/docs/built_in_transformers/standard_transformers/index.md b/docs/built_in_transformers/standard_transformers/index.md index a73b5e70..deb9869e 100644 --- a/docs/built_in_transformers/standard_transformers/index.md +++ b/docs/built_in_transformers/standard_transformers/index.md @@ -29,7 +29,6 @@ Standard transformers are ready-to-use methods that require no customization and 1. [RandomEmail](random_email.md) — generates a random email address. 1. [RandomUsername](random_username.md) — generates a random username. 1. [RandomPassword](random_password.md) — generates a random password. -1. [RandomMacAddress](random_mac_address.md) — generates a random MAC address. 1. [RandomDomainName](random_domain_name.md) — generates a random domain name. 1. [RandomURL](random_url.md) — generates a random URL. 1. [RandomMac](random_mac.md) — generates a random MAC addresses. diff --git a/docs/built_in_transformers/standard_transformers/noise_date.md b/docs/built_in_transformers/standard_transformers/noise_date.md index 1c9c66d2..3884ce00 100644 --- a/docs/built_in_transformers/standard_transformers/noise_date.md +++ b/docs/built_in_transformers/standard_transformers/noise_date.md @@ -79,3 +79,15 @@ generation - the same input will always produce the same output. column: "birthdate" default: "1990-01-01" ``` + +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
hiredate2009-01-142010-08-01
+ diff --git a/docs/built_in_transformers/standard_transformers/noise_float.md b/docs/built_in_transformers/standard_transformers/noise_float.md index 18fa2c10..3a94d2d8 100644 --- a/docs/built_in_transformers/standard_transformers/noise_float.md +++ b/docs/built_in_transformers/standard_transformers/noise_float.md @@ -45,11 +45,11 @@ In this example, the original value of `standardprice` will be noised up to `50% ``` yaml title="NoiseFloat transformer example" - schema: "purchasing" name: "productvendor" + columns_type_override: # (1) + lastreceiptcost: "float8" + standardprice: "float8" transformers: - - name: "NoiseFloat" - columns_type_override: # (1) - lastreceiptcost: "numeric" - standardprice: "numeric" + - name: "NoiseFloat" params: column: "lastreceiptcost" max_ratio: 0.15 @@ -61,3 +61,15 @@ In this example, the original value of `standardprice` will be noised up to `50% 1. The type overrides perfomed for example because the playground database does not contains any tables with float columns. + +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
lastreceiptcost50.263547.87
+ diff --git a/docs/built_in_transformers/standard_transformers/noise_int.md b/docs/built_in_transformers/standard_transformers/noise_int.md index a366e7cf..36fdc1b5 100644 --- a/docs/built_in_transformers/standard_transformers/noise_int.md +++ b/docs/built_in_transformers/standard_transformers/noise_int.md @@ -52,3 +52,14 @@ value to `10` if the noised value is lower than `10` and to `1000` if the noised min: 10 max: 1000 ``` + +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
vacationhours9969
diff --git a/docs/built_in_transformers/standard_transformers/noise_numeric.md b/docs/built_in_transformers/standard_transformers/noise_numeric.md index 19ec02ba..9fcbb7cb 100644 --- a/docs/built_in_transformers/standard_transformers/noise_numeric.md +++ b/docs/built_in_transformers/standard_transformers/noise_numeric.md @@ -5,7 +5,7 @@ Add or subtract a random fraction to the original numeric value. | Name | Description | Default | Required | Supported DB types | |-----------|---------------------------------------------------------------------------------------------------|----------|----------|--------------------| | column | The name of the column to be affected | | Yes | numeric, decimal | -| precision | The precision of the noised float value (number of digits after the decimal point) | `4` | No | - | +| decimal | The decimal of the noised float value (number of digits after the decimal point) | `4` | No | - | | min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | | max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | | min | Min threshold of noised value | | No | - | @@ -23,7 +23,7 @@ Add or subtract a random fraction to the original numeric value. The `NoiseNumeric` transformer multiplies the original numeric (or decimal) value by randomly generated value that is not higher than the `max_ratio` parameter and not less that `max_ratio` parameter and adds it to or subtracts it from -the original value. Additionally, you can specify the number of decimal digits by using the `precision` parameter. +the original value. Additionally, you can specify the number of decimal digits by using the `decimal` parameter. In case you have constraints on the numeric range, you can set the `min` and `max` parameters to specify the threshold values. The values for `min` and `max` must have the same format as the `column` parameter. Parameters min and max @@ -41,8 +41,8 @@ engines in the [Transformation engines](../transformation_engines.md) section. !!! warning Greenmask cannot parse the `numeric` type sitteng. For instance `NUMERIC(10, 2)`. You should set `min` and `max` treshholds - manually as well as allowed `precision`. This behaviour will be changed in the later versions. Grenmask will be able - to determine the precision and scale of the column and set the min and max treshholds automatically if were not set. + manually as well as allowed `decimal`. This behaviour will be changed in the later versions. Grenmask will be able + to determine the decimal and scale of the column and set the min and max treshholds automatically if were not set. ## Example: Adding noise to the purchase price @@ -56,8 +56,20 @@ In this example, the original value of `standardprice` will be noised up to `50% params: column: "lastreceiptcost" max_ratio: 0.15 - precision: 2 + decimal: 2 + max: 10000 dynamic_params: min: column: "standardprice" ``` + +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
lastreceiptcost50.263557.33
diff --git a/docs/built_in_transformers/standard_transformers/random_bool.md b/docs/built_in_transformers/standard_transformers/random_bool.md index ee19b08b..50174f70 100644 --- a/docs/built_in_transformers/standard_transformers/random_bool.md +++ b/docs/built_in_transformers/standard_transformers/random_bool.md @@ -27,3 +27,14 @@ In the following example, the `RandomBool` transformer generates a random boolea params: column: "salariedflag" ``` + +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
salariedflagtf
diff --git a/docs/built_in_transformers/standard_transformers/random_choice.md b/docs/built_in_transformers/standard_transformers/random_choice.md index 7e31d21e..d8e55938 100644 --- a/docs/built_in_transformers/standard_transformers/random_choice.md +++ b/docs/built_in_transformers/standard_transformers/random_choice.md @@ -25,14 +25,27 @@ In this example, the provided values undergo validation through PostgreSQL drive chosen from the list. ```yaml title="RandomChoice transformer example" - - schema: "humanresources" - name: "jobcandidate" - transformers: - - name: "RandomChoice" - params: - column: "modifieddate" - validate: true - values: - - "2023-12-21 07:41:06.891" - - "2023-12-21 07:41:06.896" +- schema: "humanresources" + name: "jobcandidate" + transformers: + - name: "RandomChoice" + params: + column: "modifieddate" + validate: true + engine: hash + values: + - "2023-12-21 07:41:06.891" + - "2023-12-21 07:41:06.896" ``` + +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
modifieddate2007-06-23 00:00:002023-12-21 07:41:06.891
+ diff --git a/docs/built_in_transformers/standard_transformers/random_date.md b/docs/built_in_transformers/standard_transformers/random_date.md index d8c796f5..94c4e93b 100644 --- a/docs/built_in_transformers/standard_transformers/random_date.md +++ b/docs/built_in_transformers/standard_transformers/random_date.md @@ -2,25 +2,38 @@ Generate a random date in a specified interval. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------|------------------------------| -| column | Name of the column to be affected | | Yes | date, timestamp, timestamptz | -| min | The minimum threshold date for the random value. The format depends on the column type. | | Yes | - | -| max | The maximum threshold date for the random value. The format depends on the column type. | | Yes | - | -| truncate | Truncate the date to the specified part (`nano`, `second`, `minute`, `hour`, `day`, `month`, `year`). The truncate operation is not applied by default. | | No | - | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------|------------------------------| +| column | Name of the column to be affected | | Yes | date, timestamp, timestamptz | +| min | The minimum threshold date for the random value. The format depends on the column type. | | Yes | - | +| max | The maximum threshold date for the random value. The format depends on the column type. | | Yes | - | +| truncate | Truncate the date to the specified part (`nanosecond`, `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `month`, `year`). The truncate operation is not applied by default. | | No | - | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | ## Description The `RandomDate` transformer generates a random date within the provided interval, starting from `min` to `max`. It can also perform date truncation up to the specified part of the date. The format of dates in the `min` and `max` parameters must adhere to PostgreSQL types, including `DATE`, `TIMESTAMP WITHOUT TIMEZONE`, -or `TIMESTAMP WITH TIMEZONE`. The behaviour for NULL values can be configured using the `keep_null` parameter. +or `TIMESTAMP WITH TIMEZONE`. + +!!! note + + The value of `min` and `max` parameters depends on the column type. For example, for the `date` column, the value + should be in the format `YYYY-MM-DD`, while for the `timestamp` column, the value should be in the format + `YYYY-MM-DD HH:MM:SS` or `YYYY-MM-DD HH:MM:SS.SSSSSS`. The `timestamptz` column requires the value to be in the + format `YYYY-MM-DD HH:MM:SS.SSSSSS+HH:MM`. Read more about date/time formats in + the [PostgreSQL documentation](https://www.postgresql.org/docs/current/datatype-datetime.html). + +The behaviour for `NULL` values can be configured using the `keep_null` parameter. The `engine` parameter allows you to +choose between random and hash engines for generating values. Read more about the engines in +the [Transformation engines](../transformation_engines.md) section. ## Example: Generate `modifieddate` -In the following example, a random timestamp without timezone is generated for the `modifieddate` column within the range from -`2011-05-31 00:00:00` to `2013-05-31 00:00:00`, and the part of the random value after `day` is truncated. +In the following example, a random timestamp without timezone is generated for the `modifieddate` column within the +range from `2011-05-31 00:00:00` to `2013-05-31 00:00:00`, and the part of the random value after `day` is truncated. ``` yaml title="RandomDate transformer example" - schema: "sales" @@ -35,9 +48,56 @@ In the following example, a random timestamp without timezone is generated for t truncate: "day" ``` -```bash title="Expected result" +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
modifieddate2014-06-30 00:00:002012-07-27 00:00:00
+ +## Example: Generate `hiredate` based on `birthdate` using two transformations + +In this example, the `RandomDate` transformer generates a random date for the `birthdate` column within the +range `now - 50 years` to `now - 18 years`. The hire date is generated based on the `birthdate`, ensuring that the +employee is at least 18 years old when hired. + +```yaml +- schema: "humanresources" + name: "employee" + transformers: + - name: "RandomDate" + params: + column: "birthdate" + min: '{{ now | tsModify "-50 years" | .EncodeValue }}' # 1994 + max: '{{ now | tsModify "-18 years" | .EncodeValue }}' # 2006 -| column name | original value | transformed | -|--------------|---------------------|---------------------| -| modifieddate | 2007-06-23 00:00:00 | 2005-12-08 00:00:00 | + - name: "RandomDate" + params: + column: "hiredate" + truncate: "month" + max: "{{ now | .EncodeValue }}" + dynamic_params: + min: + column: "birthdate" + template: '{{ .GetValue | tsModify "18 years" | .EncodeValue }}' # min age 18 years ``` + +Result: + + + + + + + + + + + +
ColumnOriginalValueTransformedValue
birthdate1969-01-291985-10-29
hiredate2009-01-142023-01-01
+ + diff --git a/docs/built_in_transformers/standard_transformers/random_int.md b/docs/built_in_transformers/standard_transformers/random_int.md index 9c86f55f..ecf29ccf 100644 --- a/docs/built_in_transformers/standard_transformers/random_int.md +++ b/docs/built_in_transformers/standard_transformers/random_int.md @@ -2,13 +2,13 @@ Generate a random integer within the provided interval. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|-------------------------------------------------------------------------------------------------|----------|----------|-----------------------------------------------------| -| column | The name of the column to be affected | | Yes | int2 (smallint), int4 (int), int8 (bigint), numeric | -| min | The minimum threshold for the random value. The value range depends on the column type. | | Yes | - | -| max | The maximum threshold for the random value. The value range depends on the column type. | | Yes | - | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|-------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | int2, int4, int8 | +| min | The minimum threshold for the random value | | Yes | - | +| max | The maximum threshold for the random value | | Yes | - | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | ## Dynamic parameters @@ -42,12 +42,17 @@ the `orderqty` column. max: 30 ``` -```bash title="Expected result" +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
orderqty129
-| column name | original value | transformed | -|-------------|----------------|-------------| -| orderqty | 1 | 8 | -``` ## Example: Generate random sick leave hours based on vacation hours @@ -67,3 +72,15 @@ sick leave hours based on the number of vacation hours. min: column: "vacationhours" ``` + +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
sickleavehours6999
+ diff --git a/docs/built_in_transformers/standard_transformers/random_ip.md b/docs/built_in_transformers/standard_transformers/random_ip.md index 1810ea8e..5cd97793 100644 --- a/docs/built_in_transformers/standard_transformers/random_ip.md +++ b/docs/built_in_transformers/standard_transformers/random_ip.md @@ -1,31 +1,30 @@ -The `RandomIp` transformer is designed to populate specified database columns with random IP v4 or V6 addresses. -This utility is essential for applications requiring the simulation of network data, testing systems that utilize IP +The `RandomIp` transformer is designed to populate specified database columns with random IP v4 or V6 addresses. +This utility is essential for applications requiring the simulation of network data, testing systems that utilize IP addresses, or anonymizing real IP addresses in datasets. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|----------------------------------------------------|----------|----------|---------------------| -| column | The name of the column to be affected | | Yes | text, varchar, inet | -| subnet | Subnet for generating random ip in V4 or V6 format | `false` | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` |No |- | +| Name | Description | Default | Required | Supported DB types | +|--------|-------------------------------------------------------------------------------------------------|----------|----------|---------------------| +| column | The name of the column to be affected | | Yes | text, varchar, inet | +| subnet | Subnet for generating random ip in V4 or V6 format | | Yes | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | ## Dynamic parameters -| Name | Supported types | -|------------|---------------------| -| subnet | cidr, text, varchar | +| Name | Supported types | +|--------|---------------------| +| subnet | cidr, text, varchar | ## Description -Utilizing a robust algorithm or library for generating IP addresses, the `RandomIp` transformer injects random IPv4 -or IPv6 addresses into the designated database column, depending on the provided subnet. The transformer automatically +Utilizing a robust algorithm or library for generating IP addresses, the `RandomIp` transformer injects random IPv4 +or IPv6 addresses into the designated database column, depending on the provided subnet. The transformer automatically detects whether to generate an IPv4 or IPv6 address based on the subnet version specified. - ## Example: Generate a Random IPv4 Address for a 192.168.1.0/24 Subnet -This example demonstrates how to configure the RandomIp transformer to inject a random IPv4 address into the +This example demonstrates how to configure the RandomIp transformer to inject a random IPv4 address into the ip_address column for entries in the `192.168.1.0/24` subnet: ```sql title="Create table ip_networks and insert data" @@ -50,15 +49,27 @@ VALUES ('192.168.1.10', '192.168.1.0/24'), name: ip_networks transformers: - name: "RandomIp" - params: + params: subnet: "192.168.1.0/24" column: "ip_address" engine: "random" ``` +Result: + + + + + + + + +
ColumnOriginalValueTransformedValue
ip_address192.168.1.10192.168.1.28
+ + ## Example: Generate a Random IP Based on the Dynamic Subnet Parameter -This configuration illustrates how to use the RandomIp transformer dynamically, where it reads the subnet information +This configuration illustrates how to use the RandomIp transformer dynamically, where it reads the subnet information from the network column of the database and generates a corresponding random IP address: ```yaml title="RandomPerson transformer example with dynamic mode" @@ -72,4 +83,15 @@ from the network column of the database and generates a corresponding random IP dynamic_params: subnet: column: "network" -``` \ No newline at end of file +``` + +Result: + + + + + + + + +
ColumnOriginalValueTransformedValue
ip_address192.168.1.10192.168.1.111
diff --git a/docs/built_in_transformers/standard_transformers/random_mac.md b/docs/built_in_transformers/standard_transformers/random_mac.md index f647de63..9153922b 100644 --- a/docs/built_in_transformers/standard_transformers/random_mac.md +++ b/docs/built_in_transformers/standard_transformers/random_mac.md @@ -2,18 +2,23 @@ The `RandomMac` transformer is designed to populate specified database columns w ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------|---------------------| -| column | The name of the column to be affected | | Yes | text, varchar, macaddr | -| keep_original_vendor | Should the Individual/Group (I/G) and Universal/Local (U/L) bits be preserved from the original MAC address. | `false` | No | - | -| cast_type | Param wich allow to set Individual/Group (I/G) bit in MAC Address. Allowed values [any, individual, group]. If this value is `individual`, the address is meant for a single device (unicast). If it is `group`, the address is for a group of devices, which can include multicast and broadcast addresses. | any | No | | -| management_type | Param wich allow to set Universal/Local (U/L) bit in MAC Address. Allowed values [any, universal, local]. If this bit is `universal`, the address is universally administered (globally unique). If it is `local`, the address is locally administered (such as when set manually or programmatically on a network device). | any | No | | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No |- | +| Name | Description | Default | Required | Supported DB types | +|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------|------------------------| +| column | The name of the column to be affected | | Yes | text, varchar, macaddr | +| keep_original_vendor | Should the Individual/Group (I/G) and Universal/Local (U/L) bits be preserved from the original MAC address. | `false` | No | - | +| cast_type | Param which allow to set Individual/Group (I/G) bit in MAC Address. Allowed values [any, individual, group]. If this value is `individual`, the address is meant for a single device (unicast). If it is `group`, the address is for a group of devices, which can include multicast and broadcast addresses. | any | No | | +| management_type | Param which allow to set Universal/Local (U/L) bit in MAC Address. Allowed values [any, universal, local]. If this bit is `universal`, the address is universally administered (globally unique). If it is `local`, the address is locally administered (such as when set manually or programmatically on a network device). | any | No | | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | ## Description -TODO +The `RandomMac` transformer generates a random MAC address and injects it into the specified database column. The +transformer can be configured to preserve the Individual/Group (I/G) and Universal/Local (U/L) bits from the original +MAC address. You can also keep the original vendor bits in the generated MAC address by setting +the `keep_original_vendor` parameter to `true`. +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. ## Example: Generate a Random MAC Address @@ -21,19 +26,20 @@ This example demonstrates how to configure the RandomMac transformer to inject a mac_address column: ```sql title="Create table mac_addresses and insert data" -CREATE TABLE mac_addresses ( - id SERIAL PRIMARY KEY, - device_name VARCHAR(50), - mac_address MACADDR, - description TEXT +CREATE TABLE mac_addresses +( + id SERIAL PRIMARY KEY, + device_name VARCHAR(50), + mac_address MACADDR, + description TEXT ); -INSERT INTO mac_addresses (device_name, mac_address, description) VALUES - ('Device A', '00:1A:2B:3C:4D:5E', 'Description for Device A'), - ('Device B', '01:2B:3C:4D:5E:6F', 'Description for Device B'), - ('Device C', '02:3C:4D:5E:6F:70', 'Description for Device C'), - ('Device D', '03:4D:5E:6F:70:71', 'Description for Device D'), - ('Device E', '04:5E:6F:70:71:72', 'Description for Device E'); +INSERT INTO mac_addresses (device_name, mac_address, description) +VALUES ('Device A', '00:1A:2B:3C:4D:5E', 'Description for Device A'), + ('Device B', '01:2B:3C:4D:5E:6F', 'Description for Device B'), + ('Device C', '02:3C:4D:5E:6F:70', 'Description for Device C'), + ('Device D', '03:4D:5E:6F:70:71', 'Description for Device D'), + ('Device E', '04:5E:6F:70:71:72', 'Description for Device E'); ``` @@ -47,4 +53,15 @@ INSERT INTO mac_addresses (device_name, mac_address, description) VALUES engine: "random" cast_type: "any" management_type: "any" -``` \ No newline at end of file +``` + +Result: + + + + + + + + +
ColumnOriginalValueTransformedValue
mac_address00:1a:2b:3c:4d:5eac:7f:a8:11:4e:0d
diff --git a/docs/built_in_transformers/standard_transformers/random_mac_address.md b/docs/built_in_transformers/standard_transformers/random_mac_address.md deleted file mode 100644 index 7982800e..00000000 --- a/docs/built_in_transformers/standard_transformers/random_mac_address.md +++ /dev/null @@ -1,29 +0,0 @@ -The `RandomMacAddress` transformer is developed to populate specified database columns with random MAC (Media Access Control) addresses. This transformer is particularly useful for simulating network hardware data, testing applications that process MAC addresses, or anonymizing real network device identifiers in datasets. - -## Parameters - -| Name | Description | Default | Required | Supported DB types | -|-----------|----------------------------------------------------|---------|----------|----------------------------------| -| column | The name of the column to be affected | | Yes | text, varchar, macaddr, macaddr8 | -| keep_null | Indicates whether NULL values should be preserved | `false` | No | - | - -## Description - -Utilizing a sophisticated algorithm or library for generating MAC address strings, the `RandomMacAddress` transformer injects random MAC addresses into the designated database column. Each generated MAC address follows the standard format of six groups of two hexadecimal digits, separated by colons (e. g., "01:23:45:67:89:ab"), ensuring plausible values for network device simulations. - -## Example: Populate random MAC addresses for the `network_devices` table - -This example shows how to configure the `RandomMacAddress` transformer to populate the `mac_address` column in -a `network_devices` table with random MAC addresses, enhancing the realism of simulated network device data. - -```yaml title="RandomMacAddress transformer example" -- schema: "public" - name: "network_devices" - transformers: - - name: "RandomMacAddress" - params: - column: "mac_address" - keep_null: false -``` - -With this configuration, every entry in the `mac_address` column will be assigned a random MAC address, replacing any existing non-NULL values. Setting the `keep_null` parameter to `true` allows the preservation of existing NULL values within the column, accommodating scenarios where MAC address data may be intentionally absent. diff --git a/docs/built_in_transformers/standard_transformers/random_person.md b/docs/built_in_transformers/standard_transformers/random_person.md index 69099326..2592d642 100644 --- a/docs/built_in_transformers/standard_transformers/random_person.md +++ b/docs/built_in_transformers/standard_transformers/random_person.md @@ -3,14 +3,13 @@ first name, last name, title and gender. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|------------|------------------------------------------------------|---------|----------|--------------------| -| columns | The name of the column to be affected | | Yes | text, varchar | -| gender | set specific gender (possible values: Male, Female, Any) | `Any` | No | - | -| gender_mapping | Specify gender name to possible values when using dynamic mode in "gender" parameter | `Any` |No |- | -| fallback_gender | Specify fallback gender if not mapped when using dynamic mode in "gender" parameter | `Any` |No |- | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` |No |- | - +| Name | Description | Default | Required | Supported DB types | +|-----------------|-------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| columns | The name of the column to be affected | | Yes | text, varchar | +| gender | set specific gender (possible values: Male, Female, Any) | `Any` | No | - | +| gender_mapping | Specify gender name to possible values when using dynamic mode in "gender" parameter | `Any` | No | - | +| fallback_gender | Specify fallback gender if not mapped when using dynamic mode in "gender" parameter | `Any` | No | - | +| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | ## Description @@ -18,17 +17,17 @@ The `RandomPerson` transformer utilizes a comprehensive list of first names to i designated database column. This feature allows for the creation of diverse and realistic user profiles by simulating a variety of first names without using real user data. - ### *column* object attributes * `name` — the name of the column where the personal attributes will be stored. This value is required. * `template` - the template for the column value. -You can use the next attributes: `.FirstName`, `.LastName` or `.Title`. For example, if you want to generate a full name, you can use the next template: - `"{{ .FirstName }} {{ .LastName }}"` + You can use the next attributes: `.FirstName`, `.LastName` or `.Title`. For example, if you want to generate a full + name, you can use the next template: + `"{{ .FirstName }} {{ .LastName }}"` * `hashing` - the bool value. Indicates whether the column value must be passed through the hashing function. -The default value is `false`. If all column has `hashing` set to `false` (by default), then all columns will be hashed. - + The default value is `false`. If all column has `hashing` set to `false` (by default), then all columns will be + hashed. ### *gender_mapping* object attributes @@ -67,19 +66,20 @@ the `user_profiles` table with random first names, last name, respectively. ```sql title="Create table user_profiles and insert data" -CREATE TABLE personal_data ( - id SERIAL PRIMARY KEY, - name VARCHAR(100), - surname VARCHAR(100), - sex CHAR(1) CHECK (sex IN ('M', 'F')) +CREATE TABLE personal_data +( + id SERIAL PRIMARY KEY, + name VARCHAR(100), + surname VARCHAR(100), + sex CHAR(1) CHECK (sex IN ('M', 'F')) ); -- Insert sample data into the table -INSERT INTO personal_data (name, surname, sex) VALUES - ('John', 'Doe', 'M'), - ('Jane', 'Smith', 'F'), - ('Alice', 'Johnson', 'F'), - ('Bob', 'Lee', 'M'); +INSERT INTO personal_data (name, surname, sex) +VALUES ('John', 'Doe', 'M'), + ('Jane', 'Smith', 'F'), + ('Alice', 'Johnson', 'F'), + ('Bob', 'Lee', 'M'); ``` ```yaml title="RandomPerson transformer example" @@ -97,12 +97,26 @@ INSERT INTO personal_data (name, surname, sex) VALUES engine: "hash" ``` +Result + + + + + + + + + + + +
ColumnOriginalValueTransformedValue
nameJohnZane
surnameDoeMcCullough
+ + ## Example: Populate random first name and last name for table user_profiles in dynamic mode This example demonstrates how to use the `RandomPerson` transformer to populate the `name`, `surname` using dynamic gender - ```yaml title="RandomPerson transformer example with dynamic mode" - schema: public name: personal_data @@ -119,3 +133,17 @@ gender gender: column: sex ``` + +Result: + + + + + + + + + + + +
ColumnOriginalValueTransformedValue
nameJohnMartin
surnameDoeMueller
diff --git a/docs/built_in_transformers/standard_transformers/random_string.md b/docs/built_in_transformers/standard_transformers/random_string.md index 8578fbf7..e579a62f 100644 --- a/docs/built_in_transformers/standard_transformers/random_string.md +++ b/docs/built_in_transformers/standard_transformers/random_string.md @@ -38,9 +38,13 @@ character set used for generation includes `1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ symbols: "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" ``` -```bash title="Expected result" - -| column name | original value | transformed | -|---------------|----------------|-------------| -| accountnumber | AUSTRALI0001 | 96B82A65548 | -``` +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
accountnumberAUSTRALI00014VUI6P2OZ
diff --git a/docs/built_in_transformers/standard_transformers/random_uuid.md b/docs/built_in_transformers/standard_transformers/random_uuid.md index 9d2bb9ae..0dd2084c 100644 --- a/docs/built_in_transformers/standard_transformers/random_uuid.md +++ b/docs/built_in_transformers/standard_transformers/random_uuid.md @@ -30,9 +30,13 @@ The following example replaces original UUID values of the `rowguid` column to r keep_null: false ``` -```title="Expected result" - -| column name | original value | transformed | -|-------------|--------------------------------------|--------------------------------------| -| rowguid | f01251e5-96a3-448d-981e-0f99d789110d | 0211629f-d197-4187-8a87-095ec4f51977 | -``` +Result + + + + + + + + +
ColumnOriginalValueTransformedValue
rowguidf01251e5-96a3-448d-981e-0f99d789110d8ed8c4b2-7e7a-1e8d-f0f0-768e0e8ed0d0
diff --git a/mkdocs.yml b/mkdocs.yml index e51f24ec..2e36ae26 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,7 +84,6 @@ nav: - RandomEmail: built_in_transformers/standard_transformers/random_email.md - RandomUsername: built_in_transformers/standard_transformers/random_username.md - RandomPassword: built_in_transformers/standard_transformers/random_password.md - - RandomMacAddress: built_in_transformers/standard_transformers/random_mac_address.md - RandomDomainName: built_in_transformers/standard_transformers/random_domain_name.md - RandomPerson: built_in_transformers/standard_transformers/random_person.md - RandomURL: built_in_transformers/standard_transformers/random_url.md From 9db7fe7b11b0de506817bad92311fa5978d12f4d Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Fri, 17 May 2024 11:57:02 +0300 Subject: [PATCH 15/20] doc: Refactored docs * Added random_numeric.md * Added examples --- .../standard_transformers/index.md | 2 +- .../standard_transformers/noise_date.md | 2 +- .../standard_transformers/noise_float.md | 20 ++-- .../standard_transformers/noise_int.md | 16 +-- .../standard_transformers/noise_numeric.md | 18 +-- .../standard_transformers/random_bool.md | 10 +- .../standard_transformers/random_choice.md | 2 +- .../standard_transformers/random_date.md | 9 +- .../standard_transformers/random_email.md | 24 ++-- .../standard_transformers/random_float.md | 44 +++++-- .../standard_transformers/random_int.md | 15 ++- .../standard_transformers/random_ip.md | 11 +- .../standard_transformers/random_mac.md | 2 +- .../standard_transformers/random_numeric.md | 59 +++++++++ .../standard_transformers/random_person.md | 15 ++- .../standard_transformers/random_string.md | 16 +-- .../standard_transformers/random_unix_time.md | 28 ----- .../random_unix_timestamp.md | 112 ++++++++++++++++++ .../standard_transformers/random_uuid.md | 10 +- mkdocs.yml | 3 +- 20 files changed, 297 insertions(+), 121 deletions(-) create mode 100644 docs/built_in_transformers/standard_transformers/random_numeric.md delete mode 100644 docs/built_in_transformers/standard_transformers/random_unix_time.md create mode 100644 docs/built_in_transformers/standard_transformers/random_unix_timestamp.md diff --git a/docs/built_in_transformers/standard_transformers/index.md b/docs/built_in_transformers/standard_transformers/index.md index deb9869e..29a74cff 100644 --- a/docs/built_in_transformers/standard_transformers/index.md +++ b/docs/built_in_transformers/standard_transformers/index.md @@ -19,7 +19,7 @@ Standard transformers are ready-to-use methods that require no customization and 1. [RandomUuid](random_uuid.md) — generates a random unique user ID. 1. [RandomLatitude](random_latitude.md) — generates a random latitude value. 1. [RandomLongitude](random_longitude.md) — generates a random longitude value. -1. [RandomUnixTime](random_unix_time.md) — generates a random Unix timestamp. +1. [RandomUnixTimestamp](random_unix_timestamp.md) — generates a random Unix timestamp. 1. [RandomDayOfWeek](random_day_of_week.md) — generates a random day of the week. 1. [RandomDayOfMonth](random_day_of_month.md) — generates a random day of the month. 1. [RandomMonthName](random_month_name.md) — generates the name of a random month. diff --git a/docs/built_in_transformers/standard_transformers/noise_date.md b/docs/built_in_transformers/standard_transformers/noise_date.md index 3884ce00..4b948131 100644 --- a/docs/built_in_transformers/standard_transformers/noise_date.md +++ b/docs/built_in_transformers/standard_transformers/noise_date.md @@ -10,7 +10,7 @@ Randomly add or subtract a duration within the provided `ratio` interval to the | min | Min threshold date (and/or time) of value. The value has the same format as `column` parameter | | No | - | | max | Max threshold date (and/or time) of value. The value has the same format as `column` parameter | | No | - | | truncate | Truncate the date to the specified part (`nanosecond`, `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `month`, `year`). The truncate operation is not applied by default. | | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Dynamic parameters diff --git a/docs/built_in_transformers/standard_transformers/noise_float.md b/docs/built_in_transformers/standard_transformers/noise_float.md index 3a94d2d8..6c012451 100644 --- a/docs/built_in_transformers/standard_transformers/noise_float.md +++ b/docs/built_in_transformers/standard_transformers/noise_float.md @@ -2,15 +2,15 @@ Add or subtract a random fraction to the original float value. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|---------------------------------------------------------------------------------------------------|----------|----------|--------------------| -| column | The name of the column to be affected | | Yes | float4, float8 | -| decimal | The decimal of the noised float value (number of digits after the decimal point) | `4` | No | - | -| min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | -| max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | -| min | Min threshold of noised value | | No | - | -| max | Max threshold of noised value | | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|-----------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | float4, float8 | +| decimal | The decimal of the noised float value (number of digits after the decimal point) | `4` | No | - | +| min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | +| max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | +| min | Min threshold of noised value | | No | - | +| max | Max threshold of noised value | | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Dynamic parameters @@ -59,7 +59,7 @@ In this example, the original value of `standardprice` will be noised up to `50% column: "standardprice" ``` -1. The type overrides perfomed for example because the playground database does not contains any tables with float +1. The type overrides applied for example because the playground database does not contain any tables with float columns. Result diff --git a/docs/built_in_transformers/standard_transformers/noise_int.md b/docs/built_in_transformers/standard_transformers/noise_int.md index 36fdc1b5..901779d3 100644 --- a/docs/built_in_transformers/standard_transformers/noise_int.md +++ b/docs/built_in_transformers/standard_transformers/noise_int.md @@ -2,14 +2,14 @@ Add or subtract a random fraction to the original integer value. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|---------------------------------------------------------------------------------------------------|----------|----------|--------------------| -| column | The name of the column to be affected | | Yes | int2, int4, int8 | -| min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | -| max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | -| min | Min threshold of noised value | | No | - | -| max | Min threshold of noised value | | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|-----------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | int2, int4, int8 | +| min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | +| max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | +| min | Min threshold of noised value | | No | - | +| max | Min threshold of noised value | | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Dynamic parameters diff --git a/docs/built_in_transformers/standard_transformers/noise_numeric.md b/docs/built_in_transformers/standard_transformers/noise_numeric.md index 9fcbb7cb..5de78b26 100644 --- a/docs/built_in_transformers/standard_transformers/noise_numeric.md +++ b/docs/built_in_transformers/standard_transformers/noise_numeric.md @@ -2,15 +2,15 @@ Add or subtract a random fraction to the original numeric value. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|---------------------------------------------------------------------------------------------------|----------|----------|--------------------| -| column | The name of the column to be affected | | Yes | numeric, decimal | -| decimal | The decimal of the noised float value (number of digits after the decimal point) | `4` | No | - | -| min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | -| max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | -| min | Min threshold of noised value | | No | - | -| max | Max threshold of noised value | | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|-----------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | numeric, decimal | +| decimal | The decimal of the noised float value (number of digits after the decimal point) | `4` | No | - | +| min_ratio | The minimum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | `0.05` | No | - | +| max_ratio | The maximum random percentage for noise, from `0` to `1`, e. g. `0.1` means "add noise up to 10%" | | Yes | - | +| min | Min threshold of noised value | | No | - | +| max | Max threshold of noised value | | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Dynamic parameters diff --git a/docs/built_in_transformers/standard_transformers/random_bool.md b/docs/built_in_transformers/standard_transformers/random_bool.md index 50174f70..6478727c 100644 --- a/docs/built_in_transformers/standard_transformers/random_bool.md +++ b/docs/built_in_transformers/standard_transformers/random_bool.md @@ -2,11 +2,11 @@ Generate random boolean values. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|-------------------------------------------------------------------------------------------------|----------|----------|--------------------| -| column | The name of the column to be affected | | Yes | bool | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|-----------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | bool | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Description diff --git a/docs/built_in_transformers/standard_transformers/random_choice.md b/docs/built_in_transformers/standard_transformers/random_choice.md index d8e55938..1c9f0748 100644 --- a/docs/built_in_transformers/standard_transformers/random_choice.md +++ b/docs/built_in_transformers/standard_transformers/random_choice.md @@ -8,7 +8,7 @@ Replace values randomly chosen from a provided list. | values | A list of values in any format. The string with value `\N` is considered NULL. | | Yes | - | | validate | Performs a decoding procedure via the PostgreSQL driver using the column type to ensure that values have correct type | `true` | No | | | keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Description diff --git a/docs/built_in_transformers/standard_transformers/random_date.md b/docs/built_in_transformers/standard_transformers/random_date.md index 94c4e93b..f6104e99 100644 --- a/docs/built_in_transformers/standard_transformers/random_date.md +++ b/docs/built_in_transformers/standard_transformers/random_date.md @@ -9,7 +9,14 @@ Generate a random date in a specified interval. | max | The maximum threshold date for the random value. The format depends on the column type. | | Yes | - | | truncate | Truncate the date to the specified part (`nanosecond`, `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `month`, `year`). The truncate operation is not applied by default. | | No | - | | keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | + +## Dynamic parameters + +| Parameter | Supported types | +|-----------|------------------------------| +| min | date, timestamp, timestamptz | +| max | date, timestamp, timestamptz | ## Description diff --git a/docs/built_in_transformers/standard_transformers/random_email.md b/docs/built_in_transformers/standard_transformers/random_email.md index 128413e7..2cd1af4a 100644 --- a/docs/built_in_transformers/standard_transformers/random_email.md +++ b/docs/built_in_transformers/standard_transformers/random_email.md @@ -1,19 +1,25 @@ -The `RandomEmail` transformer is designed to populate specified database columns with random email addresses. This transformer is especially useful for applications requiring the simulation of user contact data, testing email functionalities, or anonymizing real user email addresses in datasets. +The `RandomEmail` transformer is designed to populate specified database columns with random email addresses. This +transformer is especially useful for applications requiring the simulation of user contact data, testing email +functionalities, or anonymizing real user email addresses in datasets. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|------------------------------------------------------|---------|----------|--------------------| -| column | The name of the column to be affected | | Yes | text, varchar | -| keep_null | Indicates whether NULL values should be preserved | `false` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|---------------------------------------------------|---------|----------|--------------------| +| column | The name of the column to be affected | | Yes | text, varchar | +| keep_null | Indicates whether NULL values should be preserved | `false` | No | - | ## Description -Leveraging a method or library capable of generating plausible email address strings, the `RandomEmail` transformer injects random email addresses into the specified database column. It generates email addresses with varied domains and user names, offering a realistic range of email patterns suitable for filling user tables, contact lists, or any other dataset requiring email addresses without utilizing real user data. +Leveraging a method or library capable of generating plausible email address strings, the `RandomEmail` transformer +injects random email addresses into the specified database column. It generates email addresses with varied domains and +user names, offering a realistic range of email patterns suitable for filling user tables, contact lists, or any other +dataset requiring email addresses without utilizing real user data. ## Example: Populate random email addresses for the `users` table -This example illustrates configuring the `RandomEmail` transformer to populate the `email` column in the `users` table with random email addresses, thereby simulating a diverse user base without exposing real contact information. +This example illustrates configuring the `RandomEmail` transformer to populate the `email` column in the `users` table +with random email addresses, thereby simulating a diverse user base without exposing real contact information. ```yaml title="RandomEmail transformer example" - schema: "public" @@ -25,4 +31,6 @@ This example illustrates configuring the `RandomEmail` transformer to populate t keep_null: false ``` -In this setup, the `email` column will receive random email addresses for each entry, replacing any existing non-NULL values. If `keep_null` is set to `true`, then the transformer will preserve existing NULL values, maintaining the integrity of the original dataset where email information is absent. +In this setup, the `email` column will receive random email addresses for each entry, replacing any existing non-NULL +values. If `keep_null` is set to `true`, then the transformer will preserve existing NULL values, maintaining the +integrity of the original dataset where email information is absent. diff --git a/docs/built_in_transformers/standard_transformers/random_float.md b/docs/built_in_transformers/standard_transformers/random_float.md index 8dc7d4e5..511af504 100644 --- a/docs/built_in_transformers/standard_transformers/random_float.md +++ b/docs/built_in_transformers/standard_transformers/random_float.md @@ -2,13 +2,21 @@ Generate a random float within the provided interval. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|-----------------------------------------------------------------------------------------|---------|----------|---------------------------------------------------| -| column | The name of the column to be affected | | Yes | float4 (real), float8 (double precision), numeric | -| min | The minimum threshold for the random value. The value range depends on the column type. | | Yes | - | -| max | The maximum threshold for the random value. The value range depends on the column type. | | Yes | - | -| decimal | The decimal of the random float value (number of digits after the decimal point) | `4` | No | - | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|-----------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | float4, float8 | +| min | The minimum threshold for the random value. The value range depends on the column type. | | Yes | - | +| max | The maximum threshold for the random value. The value range depends on the column type. | | Yes | - | +| decimal | The decimal of the random float value (number of digits after the decimal point) | `4` | No | - | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | + +## Dynamic parameters + +| Parameter | Supported types | +|-----------|-----------------| +| min | float4, float8 | +| max | float4, float8 | ## Description @@ -16,6 +24,9 @@ The `RandomFloat` transformer generates a random float value within the provided `max`, with the option to specify the number of decimal digits by using the `decimal` parameter. The behaviour for NULL values can be configured using the `keep_null` parameter. +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. + ## Example: Generate random price In this example, the `RandomFloat` transformer generates random prices in the range from `0.1` to `7000` while @@ -24,6 +35,8 @@ maintaining a decimal of up to 2 digits. ``` yaml title="RandomFloat transformer example" - schema: "sales" name: "salesorderdetail" + columns_type_override: # (1) + "unitprice": "float8" transformers: - name: "RandomFloat" params: @@ -33,9 +46,16 @@ maintaining a decimal of up to 2 digits. decimal: 2 ``` -```bash title="Expected result" +1. The type overrides applied for example because the playground database does not contain any tables with float + columns. -| column name | original value | transformed | -|-------------|----------------|-------------| -| unitprice | 2024.994 | 6806.5 | -``` +Result: + + + + + + + + +
ColumnOriginalValueTransformedValue
unitprice2024.9944449.7
diff --git a/docs/built_in_transformers/standard_transformers/random_int.md b/docs/built_in_transformers/standard_transformers/random_int.md index ecf29ccf..5c63f909 100644 --- a/docs/built_in_transformers/standard_transformers/random_int.md +++ b/docs/built_in_transformers/standard_transformers/random_int.md @@ -2,13 +2,13 @@ Generate a random integer within the provided interval. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|-------------------------------------------------------------------------------------------------|----------|----------|--------------------| -| column | The name of the column to be affected | | Yes | int2, int4, int8 | -| min | The minimum threshold for the random value | | Yes | - | -| max | The maximum threshold for the random value | | Yes | - | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|-----------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | int2, int4, int8 | +| min | The minimum threshold for the random value | | Yes | - | +| max | The maximum threshold for the random value | | Yes | - | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Dynamic parameters @@ -53,7 +53,6 @@ Result - ## Example: Generate random sick leave hours based on vacation hours In the following example, the `RandomInt` transformer generates a random value in the range from `1` to the value of the diff --git a/docs/built_in_transformers/standard_transformers/random_ip.md b/docs/built_in_transformers/standard_transformers/random_ip.md index 5cd97793..ab13bf71 100644 --- a/docs/built_in_transformers/standard_transformers/random_ip.md +++ b/docs/built_in_transformers/standard_transformers/random_ip.md @@ -4,11 +4,11 @@ addresses, or anonymizing real IP addresses in datasets. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|--------|-------------------------------------------------------------------------------------------------|----------|----------|---------------------| -| column | The name of the column to be affected | | Yes | text, varchar, inet | -| subnet | Subnet for generating random ip in V4 or V6 format | | Yes | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| Name | Description | Default | Required | Supported DB types | +|--------|-----------------------------------------------------------------------------------------------------|----------|----------|---------------------| +| column | The name of the column to be affected | | Yes | text, varchar, inet | +| subnet | Subnet for generating random ip in V4 or V6 format | | Yes | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Dynamic parameters @@ -66,7 +66,6 @@ Result: - ## Example: Generate a Random IP Based on the Dynamic Subnet Parameter This configuration illustrates how to use the RandomIp transformer dynamically, where it reads the subnet information diff --git a/docs/built_in_transformers/standard_transformers/random_mac.md b/docs/built_in_transformers/standard_transformers/random_mac.md index 9153922b..2f14f0fa 100644 --- a/docs/built_in_transformers/standard_transformers/random_mac.md +++ b/docs/built_in_transformers/standard_transformers/random_mac.md @@ -8,7 +8,7 @@ The `RandomMac` transformer is designed to populate specified database columns w | keep_original_vendor | Should the Individual/Group (I/G) and Universal/Local (U/L) bits be preserved from the original MAC address. | `false` | No | - | | cast_type | Param which allow to set Individual/Group (I/G) bit in MAC Address. Allowed values [any, individual, group]. If this value is `individual`, the address is meant for a single device (unicast). If it is `group`, the address is for a group of devices, which can include multicast and broadcast addresses. | any | No | | | management_type | Param which allow to set Universal/Local (U/L) bit in MAC Address. Allowed values [any, universal, local]. If this bit is `universal`, the address is universally administered (globally unique). If it is `local`, the address is locally administered (such as when set manually or programmatically on a network device). | any | No | | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Description diff --git a/docs/built_in_transformers/standard_transformers/random_numeric.md b/docs/built_in_transformers/standard_transformers/random_numeric.md new file mode 100644 index 00000000..12ca9abd --- /dev/null +++ b/docs/built_in_transformers/standard_transformers/random_numeric.md @@ -0,0 +1,59 @@ +Generate a random numeric within the provided interval. + +## Parameters + +| Name | Description | Default | Required | Supported DB types | +|-----------|-------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | numeric, decimal | +| min | The minimum threshold for the random value. The value range depends on the column type. | | Yes | - | +| max | The maximum threshold for the random value. The value range depends on the column type. | | Yes | - | +| decimal | The decimal of the random numeric value (number of digits after the decimal point) | `4` | No | - | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | + +## Dynamic parameters + +| Parameter | Supported types | +|-----------|----------------------------------------------------| +| min | int2, int4, int8, float4, float8, numeric, decimal | +| max | int2, int4, int8, float4, float8, numeric, decimal | + +## Description + +The `RandomNumeric` transformer generates a random numeric value within the provided interval, starting from `min` to +`max`, with the option to specify the number of decimal digits by using the `decimal` parameter. The behaviour for +NULL values can be configured using the `keep_null` parameter. + +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. + +## Example: Generate random price + +In this example, the `RandomNumeric` transformer generates random prices in the range from `0.1` to `7000` while +maintaining a decimal of up to 2 digits. + +``` yaml title="RandomNumeric transformer example" +- schema: "sales" + name: "salesorderdetail" + transformers: + - name: "RandomNumeric" + params: + column: "unitprice" + min: 0.1 + max: 7000 + decimal: 2 +``` + +1. The type overrides applied for example because the playground database does not contain any tables with numeric + columns. + +Result: + + + + + + + + +
ColumnOriginalValueTransformedValue
unitprice2024.9944449.7
diff --git a/docs/built_in_transformers/standard_transformers/random_person.md b/docs/built_in_transformers/standard_transformers/random_person.md index 2592d642..84c003e5 100644 --- a/docs/built_in_transformers/standard_transformers/random_person.md +++ b/docs/built_in_transformers/standard_transformers/random_person.md @@ -3,13 +3,13 @@ first name, last name, title and gender. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------------|-------------------------------------------------------------------------------------------------|----------|----------|--------------------| -| columns | The name of the column to be affected | | Yes | text, varchar | -| gender | set specific gender (possible values: Male, Female, Any) | `Any` | No | - | -| gender_mapping | Specify gender name to possible values when using dynamic mode in "gender" parameter | `Any` | No | - | -| fallback_gender | Specify fallback gender if not mapped when using dynamic mode in "gender" parameter | `Any` | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------------|-----------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| columns | The name of the column to be affected | | Yes | text, varchar | +| gender | set specific gender (possible values: Male, Female, Any) | `Any` | No | - | +| gender_mapping | Specify gender name to possible values when using dynamic mode in "gender" parameter | `Any` | No | - | +| fallback_gender | Specify fallback gender if not mapped when using dynamic mode in "gender" parameter | `Any` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Description @@ -111,7 +111,6 @@ Result - ## Example: Populate random first name and last name for table user_profiles in dynamic mode This example demonstrates how to use the `RandomPerson` transformer to populate the `name`, `surname` using dynamic diff --git a/docs/built_in_transformers/standard_transformers/random_string.md b/docs/built_in_transformers/standard_transformers/random_string.md index e579a62f..19718ffa 100644 --- a/docs/built_in_transformers/standard_transformers/random_string.md +++ b/docs/built_in_transformers/standard_transformers/random_string.md @@ -2,14 +2,14 @@ Generate a random string using the provided characters within the specified leng ## Parameters -| Name | Description | Default | Required | Supported DB types | -|------------|-------------------------------------------------------------------------------------------------|--------------------------------------------------------|----------|--------------------| -| column | The name of the column to be affected | | Yes | text, varchar | -| min_length | The minimum length of the generated string | | Yes | - | -| max_length | The maximum length of the generated string | | Yes | - | -| symbols | The range of characters that can be used in the random string | `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` | No | - | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| Name | Description | Default | Required | Supported DB types | +|------------|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------|----------|--------------------| +| column | The name of the column to be affected | | Yes | text, varchar | +| min_length | The minimum length of the generated string | | Yes | - | +| max_length | The maximum length of the generated string | | Yes | - | +| symbols | The range of characters that can be used in the random string | `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` | No | - | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Description diff --git a/docs/built_in_transformers/standard_transformers/random_unix_time.md b/docs/built_in_transformers/standard_transformers/random_unix_time.md deleted file mode 100644 index cdf2c4fa..00000000 --- a/docs/built_in_transformers/standard_transformers/random_unix_time.md +++ /dev/null @@ -1,28 +0,0 @@ -The `RandomUnixTime` transformer generates random Unix time values (timestamps) for specified database columns. It is particularly useful for populating columns with timestamp data, simulating time-related data, or anonymizing actual timestamps in a dataset. - -## Parameters - -| Name | Description | Default | Required | Supported DB types | -|-----------|------------------------------------------------------|---------|----------|--------------------| -| column | The name of the column to be affected | | Yes | int4, int8, numeric | -| keep_null | Indicates whether NULL values should be preserved | `false` | No | - | - -## Description - -The `RandomUnixTime` transformer uses the `faker` library to generate random Unix timestamps. Unix time, also known as POSIX time or Epoch time, is a system for describing points in time, defined as the number of seconds elapsed since midnight Coordinated Universal Time (UTC) of January 1, 1970, not counting leap seconds. This transformer allows for the generation of timestamps that can represent any moment from the Epoch to the present or even into the future, depending on the range of the `faker` library's implementation. - -## Example: Populate random timestamps for the `registration_dates` table - -This example configures the `RandomUnixTime` transformer to apply random Unix timestamps to the `registration_date` column in a `users` table, simulating user registration times. - -```yaml title="RandomUnixTime transformer example" -- schema: "public" - name: "users" - transformers: - - name: "RandomUnixTime" - params: - column: "registration_date" - keep_null: false -``` - -In this configuration, every entry in the `registration_date` column is assigned a random Unix timestamp, replacing any existing non-NULL values. Setting `keep_null` to `true` would ensure that NULL values in the column are left unchanged. diff --git a/docs/built_in_transformers/standard_transformers/random_unix_timestamp.md b/docs/built_in_transformers/standard_transformers/random_unix_timestamp.md new file mode 100644 index 00000000..6ec8bcb8 --- /dev/null +++ b/docs/built_in_transformers/standard_transformers/random_unix_timestamp.md @@ -0,0 +1,112 @@ +The `RandomUnixTimestamp` transformer generates random Unix time values (timestamps) for specified database columns. It +is +particularly useful for populating columns with timestamp data, simulating time-related data, or anonymizing actual +timestamps in a dataset. + +## Parameters + +| Name | Description | Default | Required | Supported DB types | +|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------|--------------------| +| column | The name of the column to be affected | | Yes | int2, int4, int8 | +| min | The minimum threshold date for the random value in unix timestamp format (integer) with `sec` unit by default | | Yes | - | +| max | The maximum threshold date for the random value in unix timestamp format (integer) with `sec` unit by default | | Yes | - | +| unit | Generated unix timestamp value unit. Possible values [`second`, `millisecond`, `microsecond`, `nanosecond`] | `second` | Yes | - | +| min_unit | Min unix timestamp threshold date unit. Possible values [`second`, `millisecond`, `microsecond`, `nanosecond`] | `second` | Yes | - | +| max_unit | Min unix timestamp threshold date unit. Possible values [`second`, `millisecond`, `microsecond`, `nanosecond`] | `second` | Yes | - | +| keep_null | Indicates whether NULL values should be preserved | `false` | No | - | +| truncate | Truncate the date to the specified part (`nanosecond`, `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `month`, `year`). The truncate operation is not applied by default. | | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | + +## Description + +The `RandomUnixTimestamp` transformer generates random Unix timestamps within the provided interval, starting from `min` +to `max`. The `min` and `max` parameters are expected to be in Unix timestamp format. The `min_unit` and `max_unit` +parameters specify the unit of the Unix timestamp threshold date. The `truncate` parameter allows you to truncate the +date to the specified part of the date. The keep_null parameter allows you to specify whether NULL values should be +preserved or replaced with transformed values. + +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. + +## Example: Generate random Unix timestamps with dynamic parameters + +In this example, the `RandomUnixTimestamp` transformer generates random Unix timestamps using dynamic parameters. The +`min` parameter is set to the `created_at` column, which is converted to Unix seconds using the `TimestampToUnixSec`. +The `max` parameter is set to a fixed value. The `paid_at` column is populated with random Unix timestamps in the +range from `created_at` to `1715934239` (Unix timestamp for `2024-05-17 12:03:59`). The `unit` parameter is set to +`millisecond` because the `paid_at` column stores timestamps in milliseconds. + +```sql +CREATE TABLE transactions +( + id SERIAL PRIMARY KEY, + kind VARCHAR(255), + total DECIMAL(10, 2), + created_at TIMESTAMP, + paid_at BIGINT -- stores milliseconds since the epoch +); + +-- Inserting data with milliseconds timestamp +INSERT INTO transactions (kind, total, created_at, paid_at) +VALUES ('Sale', 199.99, '2023-05-17 12:00:00', (EXTRACT(EPOCH FROM TIMESTAMP '2023-05-17 12:05:00') * 1000)), + ('Refund', 50.00, '2023-05-18 15:00:00', (EXTRACT(EPOCH FROM TIMESTAMP '2023-05-18 15:10:00') * 1000)), + ('Sale', 129.99, '2023-05-19 10:30:00', (EXTRACT(EPOCH FROM TIMESTAMP '2023-05-19 10:35:00') * 1000)); +``` + +```yaml title="RandomUnixTimestamp transformer example" +- schema: "public" + name: "transactions" + transformers: + - name: "RandomUnixTimestamp" + params: + column: "paid_at" + max: 1715934239 + unit: "millisecond" + min_unit: "second" + max_unit: "second" + dynamic_params: + min: + column: "created_at" + cast_to: "TimestampToUnixSec" +``` + +Result: + + + + + + + + +
ColumnOriginalValueTransformedValue
paid_at16843251000001708919030732
+ +## Example: Generate simple random Unix timestamps + +In this example, the `RandomUnixTimestamp` transformer generates random Unix timestamps for the `paid_at` column in the +range from `1615934239` (Unix timestamp for `2021-03-16 12:03:59`) to `1715934239` (Unix timestamp +for `2024-05-17 12:03:59`). The `unit` parameter is set to `millisecond` because the `paid_at` column stores timestamps +in milliseconds. + +``` yaml +- schema: "public" + name: "transactions" + transformers: + - name: "RandomUnixTimestamp" + params: + column: "paid_at" + min: 1615934239 + max: 1715934239 + unit: "millisecond" +``` + +Result: + + + + + + + + +
ColumnOriginalValueTransformedValue
paid_at16843251000001655768292548
diff --git a/docs/built_in_transformers/standard_transformers/random_uuid.md b/docs/built_in_transformers/standard_transformers/random_uuid.md index 0dd2084c..62ee08c4 100644 --- a/docs/built_in_transformers/standard_transformers/random_uuid.md +++ b/docs/built_in_transformers/standard_transformers/random_uuid.md @@ -2,11 +2,11 @@ Generate random unique user ID using version 4. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|-------------------------------------------------------------------------------------------------|----------|----------|---------------------| -| column | The name of the column to be affected | | Yes | text, varchar, uuid | -| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | -| engine | The engine used for generating the values [random, hash]. Use hash for deterministic generation | `random` | No | - | +| Name | Description | Default | Required | Supported DB types | +|-----------|-----------------------------------------------------------------------------------------------------|----------|----------|---------------------| +| column | The name of the column to be affected | | Yes | text, varchar, uuid | +| keep_null | Indicates whether NULL values should be replaced with transformed values or not | `true` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Description diff --git a/mkdocs.yml b/mkdocs.yml index 2e36ae26..45705787 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,12 +69,13 @@ nav: - RandomChoice: built_in_transformers/standard_transformers/random_choice.md - RandomDate: built_in_transformers/standard_transformers/random_date.md - RandomFloat: built_in_transformers/standard_transformers/random_float.md + - RandomNumeric: built_in_transformers/standard_transformers/random_numeric.md - RandomInt: built_in_transformers/standard_transformers/random_int.md - RandomString: built_in_transformers/standard_transformers/random_string.md - RandomUuid: built_in_transformers/standard_transformers/random_uuid.md - RandomLatitude: built_in_transformers/standard_transformers/random_latitude.md - RandomLongitude: built_in_transformers/standard_transformers/random_longitude.md - - RandomUnixTime: built_in_transformers/standard_transformers/random_unix_time.md + - RandomUnixTimestamp: built_in_transformers/standard_transformers/random_unix_timestamp.md - RandomDayOfWeek: built_in_transformers/standard_transformers/random_day_of_week.md - RandomDayOfMonth: built_in_transformers/standard_transformers/random_day_of_month.md - RandomMonthName: built_in_transformers/standard_transformers/random_month_name.md From 49d45a12ce5d57009512dd112c91124f0a3f89d0 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Fri, 17 May 2024 12:43:37 +0300 Subject: [PATCH 16/20] doc: Added doc for RandomEmail transformer --- .../standard_transformers/random_email.md | 114 +++++++++++++++--- 1 file changed, 95 insertions(+), 19 deletions(-) diff --git a/docs/built_in_transformers/standard_transformers/random_email.md b/docs/built_in_transformers/standard_transformers/random_email.md index 2cd1af4a..9914e60c 100644 --- a/docs/built_in_transformers/standard_transformers/random_email.md +++ b/docs/built_in_transformers/standard_transformers/random_email.md @@ -1,36 +1,112 @@ -The `RandomEmail` transformer is designed to populate specified database columns with random email addresses. This -transformer is especially useful for applications requiring the simulation of user contact data, testing email -functionalities, or anonymizing real user email addresses in datasets. +Generate email addresses for a specified column. ## Parameters -| Name | Description | Default | Required | Supported DB types | -|-----------|---------------------------------------------------|---------|----------|--------------------| -| column | The name of the column to be affected | | Yes | text, varchar | -| keep_null | Indicates whether NULL values should be preserved | `false` | No | - | +| Name | Description | Default | Required | Supported DB types | +|----------------------|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|--------------------| +| column | The name of the column to be affected | | Yes | text, varchar | +| keep_original_domain | Keep original of the original address | `false` | No | - | +| local_part_template | The template for local part of email | | No | - | +| domain_part_template | The template for domain part of email | | No | - | +| domains | List of domains for new email | `["gmail.com", "yahoo.com", "outlook.com", "hotmail.com", "aol.com", "icloud.com", "mail.com", "zoho.com", "yandex.com", "protonmail.com", "gmx.com", "fastmail.com"]` | No | - | +| validate | Validate generated email if using template | `false` | No | - | +| max_random_length | Max length of randomly generated part of the email | `32` | No | - | +| keep_null | Indicates whether NULL values should be preserved | `false` | No | - | +| engine | The engine used for generating the values [`random`, `hash`]. Use hash for deterministic generation | `random` | No | - | ## Description -Leveraging a method or library capable of generating plausible email address strings, the `RandomEmail` transformer -injects random email addresses into the specified database column. It generates email addresses with varied domains and -user names, offering a realistic range of email patterns suitable for filling user tables, contact lists, or any other -dataset requiring email addresses without utilizing real user data. +The `RandomEmail` transformer generates random email addresses for the specified database column. By default, the +transformer generates random email addresses with a maximum length of 32 characters. The `keep_original_domain` +parameter allows you to preserve the original domain part of the email address. The `local_part_template` +and `domain_part_template` parameters enable you to specify templates for the local and domain parts of the email +address, respectively. If the `validate` parameter is set to `true`, the transformer will validate the generated email +addresses against the specified templates. The `keep_null` parameter allows you to preserve existing NULL values in the +column. -## Example: Populate random email addresses for the `users` table +The `engine` parameter allows you to choose between random and hash engines for generating values. Read more about the +engines in the [Transformation engines](../transformation_engines.md) section. -This example illustrates configuring the `RandomEmail` transformer to populate the `email` column in the `users` table -with random email addresses, thereby simulating a diverse user base without exposing real contact information. +## Templates parameters + +In each template you have access to the columns of the table by using the `{{ .column_name }}` syntax. Note that +all values are strings. For example, you can use for assembling the email address by accessing to `first_name` and +`last_name` columns `{{ .first_name | lower }}.{{ .last_name | lower }}`. + +The transformer always generates random sequences for the email, and you can use it by accessing +the `{{ .random_string }}` variable. For example, we can add random string in the end of local part +`{{ .first_name | lower }}.{{ .last_name | lower }}.{{ .random_string }}`. + +Read more about template function [Template functions](../advanced_transformers/custom_functions/index.md). + +## Random email generation using first name and last name + +In this example, the `RandomEmail` transformer generates random email addresses for the `email` column in the `account` +table. The transformer generates email addresses using the `first_name` and `last_name` columns as the local part +of the email address and adds a random string to the end of the local part with length 10 characters. The original +domain part of the email address is preserved. + +```sql +CREATE TABLE account +( + id SERIAL PRIMARY KEY, + gender VARCHAR(1) NOT NULL, + email TEXT NOT NULL NOT NULL UNIQUE, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + birth_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +INSERT INTO account (first_name, gender, last_name, birth_date, email) +VALUES ('John', 'M', 'Smith', '1980-01-01', 'john.smith@gmail.com'); +``` ```yaml title="RandomEmail transformer example" - schema: "public" - name: "users" + name: "account" transformers: - name: "RandomEmail" params: column: "email" - keep_null: false + engine: "hash" + keep_original_domain: true + local_part_template: "{{ first_name | lower }}.{{ last_name | lower }}.{{ .random_string | trunc 10 }}" ``` -In this setup, the `email` column will receive random email addresses for each entry, replacing any existing non-NULL -values. If `keep_null` is set to `true`, then the transformer will preserve existing NULL values, maintaining the -integrity of the original dataset where email information is absent. +Result: + + + + + + + + +
ColumnOriginalValueTransformedValue
emailjohn.smith@gmail.comjohn.smith.a075d99e2d@gmail.com
+ +## Simple random email generation + +In this example, the `RandomEmail` transformer generates random email addresses for the `email` column in the `account` +table. The transformer generates random email addresses with a maximum length of 10 characters. + +```yaml title="RandomEmail transformer example" +- schema: "public" + name: "account" + transformers: + - name: "RandomEmail" + params: + column: "email" + max_random_length: 10 +``` + +Result: + + + + + + + + +
ColumnOriginalValueTransformedValue
emailjohn.smith@gmail.comjohn.smith.a075d99e2d@gmail.com
From 98f7c01687ce38fedf4ed8c165a491b902e2bc0c Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Fri, 17 May 2024 15:46:11 +0300 Subject: [PATCH 17/20] doc: Added transformation_engines.md --- .../transformation_engines.md | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/docs/built_in_transformers/transformation_engines.md b/docs/built_in_transformers/transformation_engines.md index 72df5ac6..eae42258 100644 --- a/docs/built_in_transformers/transformation_engines.md +++ b/docs/built_in_transformers/transformation_engines.md @@ -1 +1,148 @@ # Transformation engine + +The greenmask provides two engines `random` and `hash`. Most of the transformers has `engine` parameters that +by default is set to `random`. Use `hash` engine when you need to generate deterministic data - the same input +will always produce the same output. + +!!! warning + + The hash engine does not guarantee the uniqueness of generated values. Although transformers such as `Hash`, + `RandomEmail`, and `RandomUuid` typically have a low probability of producing duplicate values, this depends on the + data and the methods of application. The **feature to ensure uniqueness is currently under development** + at Greenmask and is expected to be released in future updates. For the latest status, please visit the + [Greenmask roadmap](https://github.com/orgs/GreenmaskIO/projects/6). + +## Details + +### Example schema + +The next examples will be run on the following schema and sample data: + +```sql +CREATE TABLE account +( + id SERIAL PRIMARY KEY, + gender VARCHAR(1) NOT NULL, + email TEXT NOT NULL NOT NULL UNIQUE, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + birth_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +INSERT INTO account (first_name, gender, last_name, birth_date, email) +VALUES ('John', 'M', 'Smith', '1980-01-01', 'john.smith@gmail.com'); + +CREATE TABLE orders +( + id SERIAL PRIMARY KEY, + account_id INTEGER REFERENCES account (id), + total_price NUMERIC(10, 2), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + paid_at TIMESTAMP +); + +INSERT INTO orders (account_id, total_price, created_at, paid_at) +VALUES (1, 100.50, '2024-05-01', '2024-05-02'), + (1, 200.75, '2024-05-03', NULL); +``` + +### Random engine + +The random engine serves as the default engine for the greenmask. It operates using a pseudo-random number generator, +which is initialized with a random seed sourced from a cryptographically secure random number generator. Employ the +random engine when you need to generate random data and do not require reproducibility of the same transformation +results with the same input. + +The following example demonstrates how to configure the `RandomDate` transformer to generate random. + +```yaml +- schema: "public" + name: "account" + transformers: + - name: "RandomDate" + params: + column: "birth_date" + engine: "random" # (1) + min: '1970-01-01' + max: '2000-01-01' +``` + +1. `random` engine is explicitly specified, although it is the default value. + +Results: + + + + + + + + +
ColumnOriginalValueTransformedValue
birth_date1980-01-011970-02-23
+ +Keep in mind that the `random` engine is always generates different values for the same input. For instance in we run +the previous example multiple times we will get different results. + +### Hash engine + +The hash engine is designed to generate deterministic data. It uses the `SHA-3` algorithm to hash the input value. The +hash engine is particularly useful when you need to generate the same output for the same input. For example, when you +want to transform values that are used as primary or foreign keys in a database. + +For secure reason it is suggested set global greenmask salt via `GREENMASK_GLOBAL_SALT` environment variable. The salt +is added to the hash input to prevent the possibility of reverse engineering the original value from the hashed output. +The value is hex encoded with variadic length. For example, `GREENMASK_GLOBAL_SALT=a5eddc84e762e810`. +Generate a strong random salt and keep it secret. + +The following example demonstrates how to configure the `RandomInt` transformer to generate deterministic data using the +`hash` engine. The `public.account.id` and `public.orders.account_id` columns will have the same values. + +```yaml +- schema: "public" + name: "account" + transformers: + + - name: "RandomInt" + params: + column: "id" + engine: hash + min: 1 + max: 2147483647 + +- schema: "public" + name: "orders" + transformers: + + - name: "RandomInt" + params: + column: "account_id" + engine: hash + min: 1 + max: 2147483647 +``` + +Result: + +* public.account + + + + + + + + +
ColumnOriginalValueTransformedValue
id1130162079
+ +* public.orders + + + + + + + + +
ColumnOriginalValueTransformedValue
account_id1130162079
+ From 43248f6c7feb33cff74b6bd630bca389c68ab5a4 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Fri, 17 May 2024 15:48:14 +0300 Subject: [PATCH 18/20] doc: Fixed description --- docs/built_in_transformers/transformation_engines.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/built_in_transformers/transformation_engines.md b/docs/built_in_transformers/transformation_engines.md index eae42258..efed693e 100644 --- a/docs/built_in_transformers/transformation_engines.md +++ b/docs/built_in_transformers/transformation_engines.md @@ -7,10 +7,9 @@ will always produce the same output. !!! warning The hash engine does not guarantee the uniqueness of generated values. Although transformers such as `Hash`, - `RandomEmail`, and `RandomUuid` typically have a low probability of producing duplicate values, this depends on the - data and the methods of application. The **feature to ensure uniqueness is currently under development** - at Greenmask and is expected to be released in future updates. For the latest status, please visit the - [Greenmask roadmap](https://github.com/orgs/GreenmaskIO/projects/6). + `RandomEmail`, and `RandomUuid` typically have a low probability of producing duplicate values The **feature to + ensure uniqueness is currently under development** at Greenmask and is expected to be released in future updates. + For the latest status, please visit the [Greenmask roadmap](https://github.com/orgs/GreenmaskIO/projects/6). ## Details From b0aa2b483483fb1e87379af7cc28c319adc78633 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Fri, 17 May 2024 15:52:44 +0300 Subject: [PATCH 19/20] doc: Added tsModify documentation --- .../custom_functions/core_functions.md | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/built_in_transformers/advanced_transformers/custom_functions/core_functions.md b/docs/built_in_transformers/advanced_transformers/custom_functions/core_functions.md index 221ccdd7..5c792858 100644 --- a/docs/built_in_transformers/advanced_transformers/custom_functions/core_functions.md +++ b/docs/built_in_transformers/advanced_transformers/custom_functions/core_functions.md @@ -44,7 +44,8 @@ Below you can find custom core functions which are divided into categories based ### masking Replaces characters with asterisk `*` symbols depending on the provided masking rule. If the -value is `NULL`, it is kept unchanged. This function is based on [ggwhite/go-masker](https://github.com/ggwhite/go-masker). +value is `NULL`, it is kept unchanged. This function is based +on [ggwhite/go-masker](https://github.com/ggwhite/go-masker). === "Masking rules" @@ -113,7 +114,9 @@ Adds or subtracts a random duration in the provided `interval` to or from the or ### noiseFloat -Adds or subtracts a random fraction to or from the original float value. Multiplies the original float value by a provided random value that is not higher than the `ratio` parameter and adds it to the original value with the option to specify the decimal via the `decimal` parameter. +Adds or subtracts a random fraction to or from the original float value. Multiplies the original float value by a +provided random value that is not higher than the `ratio` parameter and adds it to the original value with the option to +specify the decimal via the `decimal` parameter. === "Signature" @@ -132,7 +135,8 @@ Adds or subtracts a random fraction to or from the original float value. Multipl ### noiseInt -Adds or subtracts a random fraction to or from the original integer value. Multiplies the original integer value by a provided random value that is not higher than the `ratio` parameter and adds it to the original value. +Adds or subtracts a random fraction to or from the original integer value. Multiplies the original integer value by a +provided random value that is not higher than the `ratio` parameter and adds it to the original value. === "Signature" @@ -244,3 +248,22 @@ Rounds a float value up to provided decimal. * `res` — a rounded float value * `err` — an error if there is an issue + +### tsModify + +Modify original time value by adding or subtracting the provided interval. The interval is a string in the format of +the [PostgreSQL interval](https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-INTERVAL-INPUT). + +=== "Signature" + + `tsModify(interval string, val time.Time) (time.Time, error)` + +=== "Parameters" + + * `interval` — the maximum value of `ratio` that is added to the original value. The format is the same as in the [PostgreSQL interval format](https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-INTERVAL-INPUT). + * `original` — the original time value + +=== "Return values" + + * `res` — a modified date + * `err` — an error if there is an issue From 87d5455a393abc3e4a339a6693c2c273f9d4aa16 Mon Sep 17 00:00:00 2001 From: Vadim Voitenko Date: Fri, 17 May 2024 16:00:43 +0300 Subject: [PATCH 20/20] doc: Roadmap on the main page and hash func info --- docs/built_in_transformers/transformation_engines.md | 7 +++++++ docs/index.md | 1 + 2 files changed, 8 insertions(+) diff --git a/docs/built_in_transformers/transformation_engines.md b/docs/built_in_transformers/transformation_engines.md index efed693e..3cf35497 100644 --- a/docs/built_in_transformers/transformation_engines.md +++ b/docs/built_in_transformers/transformation_engines.md @@ -4,6 +4,13 @@ The greenmask provides two engines `random` and `hash`. Most of the transformers by default is set to `random`. Use `hash` engine when you need to generate deterministic data - the same input will always produce the same output. +!!! info + + Greenmask employs the `SHA-3` algorithm to hash input values. While this function is cryptographically secure, it does + exhibit lower performance. We plan to introduce additional hash functions in the future to offer a balance between + security and performance. For example, `SipHash`, which provides a good trade-off between security and performance, is + currently in development and is expected to be included in the stable `v0.2` release of Greenmask. + !!! warning The hash engine does not guarantee the uniqueness of generated values. Although transformers such as `Hash`, diff --git a/docs/index.md b/docs/index.md index ed87f610..db8012b4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -59,6 +59,7 @@ Greenmask is ideal for various scenarios, including: ## Links +* [Greenmask Roadmap](https://github.com/orgs/GreenmaskIO/projects/6) * [Email](mailto:support@greenmask.io) * [Twitter](https://twitter.com/GreenmaskIO) * [Telegram](https://t.me/greenmask_community)