Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Online: https://sqlbuild.com/docs/concepts/adapters/snowflake/
- Session initialization
- Planning metadata
- Build metadata
- State tables
- Shared connections across targets
- Cost estimates

Expand Down Expand Up @@ -75,6 +76,18 @@ During a build, checks for whether one relation exists use `SHOW TERSE TABLES` a
need a running warehouse. Each build reuses these answers until it changes that relation, for
example by replacing a delta table or adding a column, and then reads it again.

## State tables

SQLBuild records its state in `_sqlbuild_*` tables, such as `_sqlbuild_fingerprints`,
`_sqlbuild_source_freshness`, and `_sqlbuild_microbatches`. On Snowflake, SQLBuild creates every
state table as a permanent table with `DATA_RETENTION_TIME_IN_DAYS = 90`, the maximum time travel,
so the table also has Fail-safe. On editions that allow only 1 day of time travel, Snowflake
rejects 90 and SQLBuild creates the table with 1 day instead.

The retention is set only in the statement that creates the table. SQLBuild never alters an
existing state table, so a state table that already exists keeps its current table type and
retention. Models are not affected: their table type follows the model `table_type` setting.

## Shared connections across targets

Use multiple targets to reuse one Snowflake connection while selecting different authoritative
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ SQLBuild records append-only planning evidence in the warehouse:

The planner reads the latest applicable facts and appends new facts after successful work.

On Snowflake, SQLBuild creates its state tables as permanent tables with the maximum time travel:
90 days, or 1 day on editions that allow only 1. The retention is set when SQLBuild creates the
table. See [Snowflake state tables](adapters/snowflake.md#state-tables).

## Related topics

- [Replay decisions](planning/replay-decisions.md)
Expand Down
22 changes: 14 additions & 8 deletions src/sqlbuild/adapter/contract/classes/base_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ class BaseAdapter(RetentionAdapterMixin, StrictAdapter):
sql_analysis_dialect_name: ClassVar[str | None] = None
sql_lexical_syntax: ClassVar[SqlLexicalSyntax] = SqlLexicalSyntax()
max_identifier_length: ClassVar[int] = 63
state_tables_transient: ClassVar[bool] = False
relation_grants_supported: ClassVar[bool] = True
views_read_with_reader_access: ClassVar[bool] = False
allows_implicit_managed_write_schema: ClassVar[bool] = False
Expand Down Expand Up @@ -2092,7 +2091,6 @@ def render_create_fingerprint_table_sql(
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
transient=self.state_tables_transient,
)

def render_create_microbatch_state_table_sql(self, *, database: str | None, schema: str) -> str:
Expand Down Expand Up @@ -2161,6 +2159,20 @@ def render_create_fingerprint_index_sqls(
del database, schema
return ()

def render_create_source_freshness_table_sql(self, *, database: str | None, schema: str) -> str:
"""Render DDL that creates the source freshness table when it is missing."""

from sqlbuild.compiler.source_freshness.main.create_table_sql import (
build_create_table_sql as build_source_freshness_create_table_sql,
)

return build_source_freshness_create_table_sql(
database=database,
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
)

def render_create_source_freshness_index_sqls(
self,
*,
Expand Down Expand Up @@ -2205,7 +2217,6 @@ def render_create_node_result_table_sql(
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
transient=self.state_tables_transient,
)

def render_create_node_result_index_sqls(
Expand All @@ -2230,7 +2241,6 @@ def render_create_audit_result_table_sql(self, *, database: str | None, schema:
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
transient=self.state_tables_transient,
)

def render_create_audit_result_index_sqls(
Expand All @@ -2252,7 +2262,6 @@ def render_create_janitor_event_table_sql(self, *, database: str | None, schema:
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
transient=self.state_tables_transient,
)

def render_create_migration_state_table_sql(self, *, database: str | None, schema: str) -> str:
Expand All @@ -2267,7 +2276,6 @@ def render_create_migration_state_table_sql(self, *, database: str | None, schem
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
transient=self.state_tables_transient,
)

def render_create_old_name_view_state_table_sql(
Expand All @@ -2284,7 +2292,6 @@ def render_create_old_name_view_state_table_sql(
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
transient=self.state_tables_transient,
)

def render_create_column_migration_state_table_sql(
Expand All @@ -2301,7 +2308,6 @@ def render_create_column_migration_state_table_sql(
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
transient=self.state_tables_transient,
)

def render_prune_fingerprint_history_sql(
Expand Down
13 changes: 12 additions & 1 deletion src/sqlbuild/adapter/contract/classes/duckdb_backed_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,18 @@ def render_read_latest_source_freshness_sql(
render_qualified_name=self.render_qualified_name,
)

def render_create_source_freshness_table_sql(self, *, database: str | None, schema: str) -> str:
from sqlbuild.compiler.source_freshness.main.create_table_sql import (
build_create_table_sql as build_source_freshness_create_table_sql,
)

return build_source_freshness_create_table_sql(
database=database,
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
)

def render_create_source_freshness_index_sqls(
self,
*,
Expand Down Expand Up @@ -907,7 +919,6 @@ def render_create_old_name_view_state_table_sql(
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
transient=self.state_tables_transient,
)

def render_create_column_migration_state_table_sql(
Expand Down
5 changes: 5 additions & 0 deletions src/sqlbuild/adapter/contract/classes/strict_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,11 @@ def render_create_fingerprint_index_sqls(
"""Render optional fingerprint table index DDL statements."""
...

@abstractmethod
def render_create_source_freshness_table_sql(self, *, database: str | None, schema: str) -> str:
"""Render DDL that creates the source freshness table when it is missing."""
...

@abstractmethod
def render_create_source_freshness_index_sqls(
self,
Expand Down
4 changes: 1 addition & 3 deletions src/sqlbuild/adapter/state_sql/_helpers/table_ddl.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ def render_state_table_create_sql_impl(
column_types: Mapping[str, StateSqlValueType],
required_columns: frozenset[str],
render_framework_type: Callable[[FrameworkType], str],
transient: bool,
) -> str:
"""Render create-if-missing DDL for string, integer, and text-timestamp state columns."""

Expand All @@ -28,5 +27,4 @@ def render_state_table_create_sql_impl(
for column in columns:
required: str = " NOT NULL" if column in required_columns else ""
definitions.append(f"{column} {rendered_types[column_types[column]]}{required}")
table_kind: str = "TRANSIENT TABLE" if transient else "TABLE"
return f"CREATE {table_kind} IF NOT EXISTS {qualified_name} ({', '.join(definitions)})"
return f"CREATE TABLE IF NOT EXISTS {qualified_name} ({', '.join(definitions)})"
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ def render_state_table_create_sql(
column_types: Mapping[str, StateSqlValueType],
required_columns: frozenset[str],
render_framework_type: Callable[[FrameworkType], str],
transient: bool = False,
) -> str:
"""Render create-if-missing DDL for one append-only state table."""

Expand All @@ -24,5 +23,4 @@ def render_state_table_create_sql(
column_types=column_types,
required_columns=required_columns,
render_framework_type=render_framework_type,
transient=transient,
)
13 changes: 12 additions & 1 deletion src/sqlbuild/adapters/bigquery/classes/bigquery_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,18 @@ def render_read_latest_source_freshness_sql(
render_qualified_name=self.render_qualified_name,
)

def render_create_source_freshness_table_sql(self, *, database: str | None, schema: str) -> str:
from sqlbuild.compiler.source_freshness.main.create_table_sql import (
build_create_table_sql as build_source_freshness_create_table_sql,
)

return build_source_freshness_create_table_sql(
database=database,
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
)

def render_create_source_freshness_index_sqls(
self,
*,
Expand Down Expand Up @@ -372,7 +384,6 @@ def render_create_old_name_view_state_table_sql(
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
transient=self.state_tables_transient,
)

def render_create_column_migration_state_table_sql(
Expand Down
13 changes: 12 additions & 1 deletion src/sqlbuild/adapters/databricks/classes/databricks_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,18 @@ def render_read_latest_source_freshness_sql(
render_qualified_name=self.render_qualified_name,
)

def render_create_source_freshness_table_sql(self, *, database: str | None, schema: str) -> str:
from sqlbuild.compiler.source_freshness.main.create_table_sql import (
build_create_table_sql as build_source_freshness_create_table_sql,
)

return build_source_freshness_create_table_sql(
database=database,
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
)

def render_create_source_freshness_index_sqls(
self,
*,
Expand Down Expand Up @@ -467,7 +479,6 @@ def render_create_old_name_view_state_table_sql(
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
transient=self.state_tables_transient,
)

def render_create_column_migration_state_table_sql(
Expand Down
13 changes: 12 additions & 1 deletion src/sqlbuild/adapters/postgres/classes/postgres_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,18 @@ def render_prune_fingerprint_history_sql(
")"
)

def render_create_source_freshness_table_sql(self, *, database: str | None, schema: str) -> str:
from sqlbuild.compiler.source_freshness.main.create_table_sql import (
build_create_table_sql as build_source_freshness_create_table_sql,
)

return build_source_freshness_create_table_sql(
database=database,
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
)

def render_create_source_freshness_index_sqls(
self,
*,
Expand Down Expand Up @@ -1723,7 +1735,6 @@ def render_create_old_name_view_state_table_sql(
schema=schema,
render_qualified_name=self.render_qualified_name,
render_framework_type=self.render_framework_type,
transient=self.state_tables_transient,
)

def render_create_column_migration_state_table_sql(
Expand Down
24 changes: 24 additions & 0 deletions src/sqlbuild/adapters/snowflake/_helpers/state_table_retention.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Time travel retention for SQLBuild state tables created on Snowflake."""

from __future__ import annotations

from sqlbuild.adapters.snowflake.constants import (
INVALID_PARAMETER_VALUE_ERRNO,
STATE_TABLE_RETENTION_PARAMETER,
)

_INVALID_RETENTION_MARKER: str = f"for parameter '{STATE_TABLE_RETENTION_PARAMETER}'"


def render_state_table_retention_sql(*, create_sql: str, retention_days: int) -> str:
"""Append the time travel retention clause to one state-table CREATE statement."""

return f"{create_sql} {STATE_TABLE_RETENTION_PARAMETER} = {retention_days}"


def is_invalid_retention_error(error: Exception) -> bool:
"""Return whether Snowflake rejected the value as ``001008 ... for parameter '<retention>'``."""

return getattr(
error, "errno", None
) == INVALID_PARAMETER_VALUE_ERRNO and _INVALID_RETENTION_MARKER in str(error)
Loading
Loading