Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure Data Factory Dataset — Snowflake Terraform Module

A typed Data Factory dataset describing a table in a Snowflake schema, targeting hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Caveat


🧩 Overview

  • ❄️ Creates one azurerm_data_factory_dataset_snowflake — a named description of a table in a Snowflake schema that pipelines read from and write to.
  • 🧬 The Data Factory dataset family's biggest outlier. Its schema_column block is {name, type, precision, scale} — there is no description field — where ten of the twelve siblings use {name, type, description}.
  • 🔤 Its column types are 32 Snowflake SQL types that share not one value with the fifteen Data Factory types the siblings use. String is legal there and refused here; VARCHAR is legal here and refused there.
  • 🔢 Carries precision and scale, which no other dataset resource has — validated only as non-negative integers, and never checked against each other or the column's type.
  • 🗂️ Has a separate schema_name, which the SQL Server Table, MySQL and PostgreSQL datasets lack entirely.

💡 Why it matters: nothing about this resource's column schema can be copied from a sibling in either direction. The harness proves it exhaustively — all 32 Snowflake types and all 15 Data Factory types driven against both this module and a sibling, each accepted by exactly one side.


❤️ Support this project

If this module saved you time:


🗺️ Where this fits in the family

flowchart TB
  RG["terraform-azurerm-resource-group"]
  ADF["terraform-azurerm-data-factory"]
  LS["a Snowflake linked service, which chooses the account, warehouse and database"]
  THIS["terraform-azurerm-data-factory-dataset-snowflake"]
  TRIO["the schema-identical trio: sql-server-table, mysql, postgresql"]
  BIN["terraform-azurerm-data-factory-dataset-binary"]
  PIPE["a Data Factory pipeline"]
  TBL["a table in a Snowflake schema"]

  RG -->|"name"| ADF
  ADF -->|"id"| THIS
  ADF -->|"id"| TRIO
  ADF -->|"id"| BIN
  LS -->|"linked_service_name, a bare NAME"| THIS
  THIS -->|"schema_column of name, type, precision, scale"| PIPE
  TRIO -->|"schema_column of name, type, description"| PIPE
  BIN -->|"no schema_column block at all"| PIPE
  THIS -->|"32 SNOWFLAKE SQL types, sharing NOT ONE value"| TRIO
  PIPE -->|"reads only at run time, never here"| TBL

  classDef this fill:#0078D4,stroke:#004578,color:#ffffff,stroke-width:2px
  classDef keystone fill:#004578,stroke:#00243d,color:#ffffff,stroke-width:2px
  classDef sibling fill:#eef3f8,stroke:#b9c8d8,color:#1b2733
  class THIS this
  class ADF keystone
  class RG,LS,TRIO,BIN,PIPE,TBL sibling
Loading

Three edges, three different column contracts: this resource's {name, type, precision, scale}, the trio's {name, type, description}, and the Binary dataset's absence of the block altogether.


🧬 What this module builds

flowchart TB
  subgraph INPUTS["Inputs"]
    NAME["name (force-new)"]
    ADFID["data_factory_id (force-new)"]
    LSNAME["linked_service_name, a bare NAME"]
    SCH["schema_name, a SEPARATE argument most siblings lack"]
    TBL["table_name, unqualified"]
    COLS["schema_column of name, type, precision, scale, with NO description"]
    META["parameters, additional_properties, annotations, description, folder"]
  end

  THIS["azurerm_data_factory_dataset_snowflake.this"]

  subgraph OUTPUTS["Outputs"]
    OID["id and name"]
    OQT["qualified_table_name, a DISPLAY value only"]
    ODOT["table_name_looks_qualified, the shape a sibling needs and this one does not"]
    OCASE["schema_name_is_lower_case and table_name_is_lower_case"]
    ONUM["numeric_column_definitions_look_wrong, plus its three components"]
    OFACTS["the outlier facts, including the disjoint type vocabulary"]
  end

  NAME --> THIS
  ADFID --> THIS
  LSNAME --> THIS
  SCH --> THIS
  TBL --> THIS
  COLS --> THIS
  META --> THIS

  THIS --> OID
  SCH --> OQT
  TBL --> OQT
  TBL --> ODOT
  SCH --> OCASE
  TBL --> OCASE
  COLS --> ONUM
  THIS --> OFACTS

  classDef this fill:#0078D4,stroke:#004578,color:#ffffff,stroke-width:2px
  classDef sibling fill:#eef3f8,stroke:#b9c8d8,color:#1b2733
  class THIS this
  class NAME,ADFID,LSNAME,SCH,TBL,COLS,META,OID,OQT,ODOT,OCASE,ONUM,OFACTS sibling
Loading
Resource Count Notes
azurerm_data_factory_dataset_snowflake.this 1 The keystone. A real ARM child of the factory.
schema_column dynamic, 0..n {name, type, precision, scale} — no description.
timeouts dynamic, 0..1 All four keys exist.

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None. The caller configures the provider, including the mandatory features {} block.

Schema notes that bite — verified against the live provider source, not inferred from the schema:

  • 🔴 THE COLUMN TYPE SETS ARE DISJOINT. This resource accepts 32 Snowflake SQL types; ten siblings accept 15 Data Factory types; they share not one value. Both are compared case-sensitively.
  • 🔴 THE schema_column BLOCK HAS NO description FIELD. A column copied from any sibling is a type error at parse time, not a validation failure — the attribute does not exist on the object.
  • 🔴 FOUR TYPE VALUES ARE NOT PLAIN WORDS. DOUBLE PRECISION contains a space; TIMESTAMP_LTZ, TIMESTAMP_NTZ and TIMESTAMP_TZ contain underscores. A list rebuilt by matching letters and digits alone silently loses all four and looks complete.
  • 🔴 precision and scale are validated only as integers ≥ 0. They are never compared with each other or with the type, so a scale larger than its precision plans and applies cleanly and is rejected by Snowflake at run time.
  • ⚠️ schema_name is a separate argument here. The SQL Server Table, MySQL and PostgreSQL datasets have none — a schema there goes inside table_name. A dotted table_name copied from one of those reaches Snowflake as a single identifier.
  • ⚠️ Snowflake folds unquoted identifiers to upper case. public becomes PUBLIC unless the object was created with quotes. This module reports the case rather than normalising it.
  • ⚠️ Only name and data_factory_id force replacement.
  • ⚠️ Timeouts default to 30m / 5m / 30m / 30m.
  • ℹ️ The requires-import error names this resource correctly — the SQL Server Table dataset's names a type that does not exist, and the JSON dataset's names the delimited-text type.
  • ℹ️ The registry page runs FLOAT and FLOAT4 together without a separator in its type list. Both are real, distinct values in the provider source.

🔑 Required Azure RBAC Roles / Permissions

Permission Scope Why
Microsoft.DataFactory/factories/datasets/write the Data Factory Create and update.
Microsoft.DataFactory/factories/datasets/read the Data Factory Refresh and plan.
Microsoft.DataFactory/factories/datasets/delete the Data Factory Destroy — see the warning below.
Data Factory Contributor the Data Factory The built-in role containing all three.

🔒 No permission on Snowflake is required or used, and none is grantable through Azure RBAC — Snowflake has its own role model entirely outside Azure. Access belongs to the linked service, so whoever can write datasets can describe any table in whatever database and warehouse that linked service points at.

💰 Creating a dataset consumes no Snowflake credits. It starts no warehouse and runs no query. Credits are consumed when a pipeline uses the dataset, which is outside this module.

⚠️ Delete is the operation to review, not create. Removing a dataset breaks every pipeline referencing it by name.


Azure Prerequisites

  • An existing Data Factory, and its Resource ID.
  • A Snowflake linked service already configured in that factory, and its name. It chooses the account, warehouse and database. Nothing here verifies it.
  • A schema and table for the dataset to describe — or parameters. Neither is enforced.
  • A dataset name meeting Microsoft's Data Factory naming rules. The provider checks only non-emptiness.
  • The Microsoft.DataFactory resource provider registered in the subscription.

📁 Module Structure

terraform-azurerm-data-factory-dataset-snowflake/
├── providers.tf    # required_version + the pinned azurerm; no provider block
├── variables.tf    # 12 typed inputs, 14 validations
├── main.tf         # the keystone, the numeric analysis, two dynamic blocks
├── outputs.tf      # 64 outputs; id first
├── README.md       # this file
├── SCOPE.md        # the cross-module contract
├── LICENSE         # MIT
└── .gitignore

⚙️ Quick Start

provider "azurerm" {
  features {}
}

module "orders" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_orders"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name

  schema_name = "ANALYTICS"
  table_name  = "ORDERS"
}

ℹ️ The schema has its own argument here. Do not write ANALYTICS.ORDERS into table_name — that is the shape three siblings need and this resource does not.


🔌 Cross-Module Contract

Consumes

Input Type Typical source
data_factory_id string terraform-azurerm-data-factory → id
linked_service_name string a Snowflake linked service's name
schema_name, table_name string, optional the caller

Emits (selected — 64 in total)

Output Description
id The dataset's Resource ID.
qualified_table_name schema.table — a display value only.
table_name_looks_qualified A dot in table_name — a sibling's shape, not this one's.
numeric_column_definitions_look_wrong The one output to assert on.
columns_with_scale_exceeding_precision Accepted at plan, rejected by Snowflake.
the_schema_column_type_set_is_SNOWFLAKE_SQL_not_data_factory Constant. Two disjoint vocabularies.
the_schema_column_block_here_has_NO_description_field Constant. A copied column is a type error.

📚 Example Library

1 · The minimum useful call
module "orders" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_orders"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name

  schema_name = "ANALYTICS"
  table_name  = "ORDERS"
}

💡 Upper case on purpose — Snowflake folds unquoted identifiers to upper case when objects are created.

2 · A dataset that resolves to nothing
module "resolves_to_nothing" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_empty"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name
  # no table, no parameters
}

check "dataset_points_somewhere" {
  assert {
    condition     = !module.resolves_to_nothing.describes_no_specific_table
    error_message = "ds_empty names no table and exposes no parameters; no pipeline can resolve it."
  }
}
3 · A column list copied from a sibling will not plan
# ❌ NOT ACCEPTED — twice over
module "copied_from_a_sibling" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_copied"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name

  schema_column = [
    { name = "ORDER_ID", type = "Int64", description = "Primary key." },
  ]
}

⚠️ Two independent failures: description does not exist on this block (a type error at parse time), and Int64 is a Data Factory type this resource does not accept. Use precision/scale and a Snowflake type.

4 · The same list, written for Snowflake
module "orders" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_orders"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name
  schema_name         = "ANALYTICS"
  table_name          = "ORDERS"

  schema_column = [
    { name = "ORDER_ID", type = "NUMBER", precision = 38, scale = 0 },
    { name = "AMOUNT", type = "NUMBER", precision = 18, scale = 2 },
    { name = "CUSTOMER", type = "VARCHAR" },
    { name = "PLACED_AT", type = "TIMESTAMP_NTZ" },
  ]
}

🔒 TIMESTAMP_NTZ carries underscores, and DOUBLE PRECISION carries a space. Copy the type list; a list rebuilt by matching letters and digits silently loses all four such values.

5 · A scale larger than its precision — accepted here, rejected by Snowflake
module "silently_wrong" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_silently_wrong"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name

  schema_column = [
    { name = "AMOUNT", type = "NUMBER", precision = 2, scale = 9 },
  ]
}

check "numeric_columns_are_coherent" {
  assert {
    condition     = !module.silently_wrong.numeric_column_definitions_look_wrong
    error_message = "A column's scale exceeds its precision; Snowflake will reject it at run time."
  }
}

⚠️ The provider validates each with IntAtLeast(0) and never compares them. This plans and applies cleanly. columns_with_scale_exceeding_precision names the offender.

6 · A precision on a type that has no use for one
module "harmless_but_odd" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_odd"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name

  schema_column = [
    { name = "CUSTOMER", type = "VARCHAR", precision = 5 },
  ]
}

output "reported" {
  value = module.harmless_but_odd.columns_with_precision_on_a_non_numeric_type # ["CUSTOMER"]
}

ℹ️ Accepted and ignored by Snowflake. Reported rather than refused: which types honour precision is Snowflake's rule, not the provider's, and refusing would reject a configuration the provider accepts.

7 · A scale with no precision beside it
module "default_precision" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_default_precision"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name

  schema_column = [
    { name = "AMOUNT", type = "NUMBER", scale = 4 },
  ]
}

⚠️ Legal, and almost never intended — Snowflake applies its default precision, which is rarely what someone specifying a scale had in mind. columns_with_scale_but_no_precision reports it.

8 · A dotted table name is a sibling's shape, not this one's
module "wrong_shape" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_wrong_shape"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name

  table_name = "ANALYTICS.ORDERS" # the schema belongs in schema_name
}

check "table_name_is_not_qualified" {
  assert {
    condition     = !module.wrong_shape.table_name_looks_qualified
    error_message = "table_name contains a dot; on this resource the schema belongs in schema_name."
  }
}

⚠️ Correct on the SQL Server Table, MySQL and PostgreSQL datasets, where the schema has nowhere else to go. Here it reaches Snowflake as a single identifier. Reported, never refused — a quoted Snowflake identifier may legitimately contain a dot.

9 · Identifier case, reported and not corrected
module "lower_case" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_lower"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name

  schema_name = "analytics"
  table_name  = "orders"
}

output "case_check" {
  value = {
    schema = module.lower_case.schema_name_is_lower_case # true
    table  = module.lower_case.table_name_is_lower_case  # true
  }
}

ℹ️ Snowflake stores analytics as ANALYTICS unless it was created with quotes. This module does not normalise the value — upper-casing it would silently break every quoted-identifier deployment, which is the harder case to debug.

10 · A parameterised dataset
module "any_table" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_any_table"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name

  parameters = {
    schemaName = "ANALYTICS"
    tableName  = ""
  }
}

💡 With parameters set, describes_no_specific_table is false even with no table.

11 · Many tables from one map
locals {
  tables = {
    orders    = { schema = "ANALYTICS", table = "ORDERS" }
    customers = { schema = "ANALYTICS", table = "CUSTOMERS" }
    ledger    = { schema = "FINANCE", table = "LEDGER" }
  }
}

module "tables" {
  for_each = local.tables

  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_${each.key}"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name
  schema_name         = each.value.schema
  table_name          = each.value.table

  folder = "bronze/snowflake"
}

check "no_table_name_is_qualified" {
  assert {
    condition     = alltrue([for m in module.tables : !m.table_name_looks_qualified])
    error_message = "A table_name contains a dot; the schema belongs in schema_name."
  }
}

💡 The provider takes no lock on the Data Factory for this resource, so these are created concurrently and safely.

12 · `folder` is not a Snowflake schema
module "orders" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_orders"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name

  schema_name = "ANALYTICS"        # WHERE THE TABLE IS, in Snowflake
  folder      = "bronze/snowflake" # where the DATASET appears, in the portal
}

⚠️ Two arguments on this resource sound like "a container the table lives in", and only one of them is about the database. Swapping them applies cleanly and points the dataset at the wrong place.

13 · Annotations, description and the escape hatch
module "orders" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_orders"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name
  schema_name         = "ANALYTICS"
  table_name          = "ORDERS"

  description = "Order header rows, read hourly by the curated pipeline."
  annotations = ["bronze", "hourly"]

  additional_properties = {
    "structure" = "[]"
  }
}

ℹ️ This resource has a dataset-level description and no per-column one — its schema_column carries precision and scale where the rest of the family carries description.

14 · Custom timeouts
module "orders" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_orders"
  data_factory_id     = var.data_factory_id
  linked_service_name = var.snowflake_linked_service_name
  table_name          = "ORDERS"

  timeouts = {
    create = "45m"
    read   = "10m"
    update = "45m"
    delete = "45m"
  }
}

ℹ️ Defaults are 30m / 5m / 30m / 30m. Terraform silently discards an object key the type does not declare.

15 · 🏗️ End-to-end composition
provider "azurerm" {
  features {}
}

module "rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-analytics-eastus"
  location = "eastus"
}

module "adf" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory.git?ref=v1.0.0"

  name                = "adf-analytics-eastus"
  resource_group_name = module.rg.name
  location            = module.rg.location

  identity = {
    type = "SystemAssigned"
  }
}

# No linked-service module exists in this suite yet, so the linked service is
# declared directly. Its connection string carries the Snowflake credential and
# chooses the ACCOUNT, WAREHOUSE and DATABASE -- keep it in a secret store and
# pass a reference; never inline it.
resource "azurerm_data_factory_linked_service_snowflake" "warehouse" {
  name            = "ls_snowflake_analytics"
  data_factory_id = module.adf.id

  connection_string = var.snowflake_connection_string # sensitive; supplied out of band
}

module "orders" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-dataset-snowflake.git?ref=v1.0.0"

  name                = "ds_orders"
  data_factory_id     = module.adf.id
  linked_service_name = azurerm_data_factory_linked_service_snowflake.warehouse.name

  schema_name = "ANALYTICS"
  table_name  = "ORDERS"
  folder      = "bronze/snowflake"

  schema_column = [
    { name = "ORDER_ID", type = "NUMBER", precision = 38, scale = 0 },
    { name = "AMOUNT", type = "NUMBER", precision = 18, scale = 2 },
    { name = "PLACED_AT", type = "TIMESTAMP_NTZ" },
  ]

  annotations = ["bronze", "hourly"]
}

check "wiring_is_consistent" {
  assert {
    condition     = !module.orders.numeric_column_definitions_look_wrong
    error_message = "A numeric column definition is incoherent."
  }
  assert {
    condition     = !module.orders.table_name_looks_qualified
    error_message = "table_name contains a dot; the schema belongs in schema_name."
  }
  assert {
    condition     = !module.orders.describes_no_specific_table
    error_message = "The dataset resolves to nothing."
  }
}

output "dataset_id" {
  value = module.orders.id
}

output "reads_from" {
  value = module.orders.qualified_table_name
}

🔒 The Snowflake credential belongs to the linked service, provisioned out of band. This module accepts none and emits none. Note that sensitive = true redacts plan output and does not encrypt state — the protection that matters is an encrypted, access-controlled backend.


📥 Inputs

Required: name, data_factory_id, linked_service_name. What it points at: schema_name, table_name, parameters. Shape: schema_column ({name, type, precision, scale}), additional_properties. Metadata: description, folder, annotations. Tail: timeouts. There is no tags variable — the resource supports none.

Full input schemas
Name Type Default Notes
name string — Force-new. Non-empty only; a leading / is refused.
data_factory_id string — Force-new. Anchored Resource-ID validator.
linked_service_name string — A bare name. Chooses the account, warehouse and database.
schema_name string null A separate argument most of the family lacks.
table_name string null Unqualified — the schema goes in schema_name.
schema_column list(object({ name, type, precision, scale })) [] No description. 32 Snowflake types, case-sensitive.
parameters map(string) {} Supplied per pipeline run.
additional_properties map(string) {} Unvalidated top-level keys.
annotations list(string) [] Not tags.
description string null The dataset's description. Empty string refused.
folder string null The authoring tree — not a Snowflake schema.
timeouts object({ create, read, update, delete }) null 30m / 5m / 30m / 30m.

🧾 Outputs

Output Description Notes
id The dataset's Resource ID. First, by convention.
name, data_factory_id, data_factory_name Identity and parent. Name parsed from the end of the ID.
resource_group_name, subscription_id Where the factory lives.
linked_service_name The linked service, as supplied. Chooses account, warehouse, database.
the_linked_service_chooses_the_account_warehouse_and_database Constant. Why this dataset is so small.
this_resource_is_the_dataset_familys_biggest_outlier Constant. The summary.
the_schema_column_block_here_has_NO_description_field Constant. A copied column is a type error.
the_schema_column_type_set_is_SNOWFLAKE_SQL_not_data_factory Constant. Two disjoint vocabularies.
four_of_the_type_values_are_not_plain_words Constant. A space and three underscores.
this_resource_has_a_separate_schema_name_argument Constant.
schema_name, table_name, has_schema_name, has_table_name What it points at.
qualified_table_name schema.table. A display value only.
describes_no_specific_table No table and no parameters. Assert on this.
table_name_looks_qualified A dot in table_name. Assert on this.
schema_name_is_lower_case, table_name_is_lower_case Identifier folding. Reported, not corrected.
parameters, parameter_count The run-time inputs.
schema_column_count, has_schema_columns, schema_column_names, untyped_schema_column_names The column schema.
columns_with_precision, columns_with_scale Fields no sibling has.
columns_with_precision_on_a_non_numeric_type Accepted and ignored.
columns_with_scale_exceeding_precision Rejected by Snowflake at run time. The certain error.
columns_with_scale_but_no_precision Legal, rarely intended.
numeric_column_definitions_look_wrong Any of the three. Assert on this.
precision_and_scale_are_validated_only_as_non_negative_integers Constant.
uses_additional_properties, additional_property_count The escape hatch.
annotations, annotation_count, has_annotations A list, not tags.
description, has_description, folder, has_folder Metadata.
folder_and_schema_name_mean_different_things Constant.
force_new_fields, fields_that_can_change_after_creation, fields_azure_returns_on_read The change surface.
import_address, the_import_guard_names_this_resource_correctly Imports. Two siblings get this wrong.
this_dataset_moves_no_data_by_itself, creating_this_consumes_no_snowflake_credits Constants.
the_module_cannot_see_which_pipelines_use_this Constant. Read before destroying.
the_module_cannot_verify_the_table_exists, no_credential_is_configured_here Constants.
destroying_the_factory_destroys_this_dataset_too, destroying_this_does_not_touch_the_table Constants.
lifecycle_prevent_destroy_is_not_available_to_a_module_caller Constant.
this_is_a_real_azure_resource_not_a_composite, the_provider_takes_no_lock_on_the_data_factory Constants.
this_resource_supports_no_azure_resource_tags Constant. Why there is no tags variable.
no_secret_is_accepted_or_emitted_by_this_module Constant.

No output is sensitive, and none can be.


🧠 Architecture Notes

Two column vocabularies that share nothing. Ten of the twelve Data Factory dataset resources accept fifteen Data Factory types — Byte, Int64, String and so on. This one accepts thirty-two Snowflake SQL types — NUMBER, VARCHAR, GEOGRAPHY. Not one value appears in both sets, and both are compared case-sensitively, so a column list is refused in whichever direction it is copied. The offline harness proves this exhaustively rather than asserting it: every one of the 47 values is driven against both this module and a Data Factory-typed sibling, and each must be accepted by exactly one side.

The block is a different shape as well as a different vocabulary. There is no description field here — precision and scale sit in its place. That makes a copied column a type error at parse time, which is the better failure: it names the attribute rather than producing a confusing validation message.

Four type values are not plain words. DOUBLE PRECISION has a space; three TIMESTAMP_* variants have underscores. Any list rebuilt by pattern-matching letters and digits loses exactly those four and looks complete — which is a mistake this library made once while extracting the set, and the reason the module carries the list verbatim.

precision and scale are barely validated. The provider applies IntAtLeast(0) to each and never compares them to each other or to the type. So scale > precision plans cleanly and Snowflake refuses it at run time; a precision on a VARCHAR is accepted and ignored; a scale with no precision takes Snowflake's default. The module reports all three separately, because only the first is certainly an error — collapsing them would overstate the other two.

The schema has its own argument here, and does not on three siblings. A dotted table_name is the correct shape on the SQL Server Table, MySQL and PostgreSQL datasets and the wrong one here, where it reaches Snowflake as a single identifier. qualified_table_name is emitted for display and documented as such, because writing it back into table_name is exactly the mistake.

Identifier case is reported, not corrected. Snowflake folds unquoted identifiers to upper case, so public becomes PUBLIC — unless the object was created with quotes, in which case the lower-case value is right. Normalising would silently break the quoted case, which is the harder one to diagnose.

Only identity forces replacement, and the linked service is not force-new — so moving a dataset to a different Snowflake account, warehouse and database is an in-place attribute change no destroy-scan will surface.


🧱 Design Principles

Concern This module's default Opt-out
Secrets None accepted, none emitted. The connection string lives on the linked service. Not available — by design.
Snowflake access No permission required or used, and none is grantable through Azure RBAC. —
Snowflake credits None consumed. A dataset starts no warehouse. —
Column types Enforced against the provider's case-sensitive 32. None — the set is closed.
A Data Factory type Refused, with the disjointness named in the error message. None.
A per-column description Refused by the type — the attribute does not exist. Use the dataset-level description.
scale > precision Reported, not refused — the provider accepts it. Ignore the output.
A precision on a non-numeric type Reported, not refused. Ignore the output.
A dotted table_name Reported, not refused — a quoted identifier may contain a dot. Ignore the output.
Identifier case Reported, never normalised. Ignore the output.
tags Not offered — the resource supports none. Tag the factory.

🔒 There is no risky toggle to close here: the resource holds no credential and touches no data. The exposures are the delete, the numeric definitions the provider does not check, and the Snowflake reach the linked service confers — the module documents the first and reports the second.


🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module with ?ref=v1.0.0 — never a branch. This library is plan-only: a human applies from CI.


🧪 Testing

terraform plan and the module's own validation {} blocks cover, offline and without credentials — note it is plan and not terraform validate, which through a module call evaluates no variable values and reports success:

  • all 32 Snowflake types accepted at their exact casing, including the space and underscore values;
  • every Data Factory type refused, with the disjointness named in the message;
  • precision and scale refused when negative or fractional, and accepted at zero;
  • the anchored Resource-ID shape of data_factory_id, and the refusal of an ID passed as linked_service_name;
  • every derived flag — the three numeric mismatches and their roll-up, table_name_looks_qualified, both identifier-case flags and describes_no_specific_table — each with one fixture per branch through terraform console.

The disjointness is executed, not asserted. The harness drives all 47 type values against both this module and a Data Factory-typed sibling — 94 module-runs — and requires each value to be accepted by exactly one side.

Only a real plan or apply can tell you whether the linked service exists, whether the table exists, whether the identifier case matches, or whether a numeric definition Snowflake dislikes will be rejected.


💬 Example Output

dataset_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-analytics-eastus/providers/Microsoft.DataFactory/factories/adf-analytics-eastus/datasets/ds_orders"
reads_from = "ANALYTICS.ORDERS"

linked_service_name                   = "ls_snowflake_analytics"
has_schema_name                       = true
describes_no_specific_table           = false
table_name_looks_qualified            = false
schema_name_is_lower_case             = false
schema_column_names                   = ["ORDER_ID", "AMOUNT", "PLACED_AT"]
columns_with_precision                = ["ORDER_ID", "AMOUNT"]
columns_with_scale_exceeding_precision = []
numeric_column_definitions_look_wrong = false
force_new_fields                      = ["name", "data_factory_id"]

🔍 Troubleshooting

Symptom Cause Fix
every schema_column type must be one of the THIRTY-TWO SNOWFLAKE SQL TYPES on "Int64" A Data Factory type. The two sets share no values. Use a Snowflake type — NUMBER here, not Int64.
An unsupported-attribute error on description inside schema_column This block has no description field. Remove it; use precision/scale, or the dataset-level description.
"varchar" is refused but "VARCHAR" works Case-sensitive, and these values are upper-case. Copy the list from the documentation.
DOUBLE PRECISION or TIMESTAMP_NTZ seems missing from a generated list A pattern matching letters and digits alone drops the space and the underscores. Copy the list; do not regenerate it.
A pipeline fails on a numeric column that planned cleanly scale exceeds precision; the provider never compares them. Check columns_with_scale_exceeding_precision.
A precision appears to do nothing It is set on a type Snowflake does not apply it to. Check columns_with_precision_on_a_non_numeric_type.
A NUMBER column has an unexpected precision A scale was given with no precision; Snowflake used its default. Check columns_with_scale_but_no_precision.
A pipeline cannot find the table, and the names look right Snowflake folds unquoted identifiers to upper case. Check schema_name_is_lower_case and table_name_is_lower_case.
A configuration copied from a MySQL or PostgreSQL dataset resolves to nothing Its table_name carried the schema; here that is one identifier. Split it: schema into schema_name.
A dataset applies but no pipeline can use it No table and no parameters. Check describes_no_specific_table.
The dataset suddenly reads from a different database linked_service_name changed — an in-place update. Review in-place dataset changes, not only replacements.
A timeouts key seems to have no effect Terraform silently discards an undeclared object key. Compare against object({ create, read, update, delete }).

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."