Skip to content

Commit 69a5a37

Browse files
fix: Handle reserved words in db, schema and table names with the normalize_name dialect helper
1 parent 0365442 commit 69a5a37

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

Diff for: target_snowflake/connector.py

+47
Original file line numberDiff line numberDiff line change
@@ -634,3 +634,50 @@ def _adapt_column_type(
634634
sql_type,
635635
)
636636
raise
637+
638+
def get_fully_qualified_name(
639+
self,
640+
table_name: str | None = None,
641+
schema_name: str | None = None,
642+
db_name: str | None = None,
643+
delimiter: str = ".",
644+
) -> str:
645+
"""Concatenates a fully qualified name from the parts.
646+
647+
Snowflake-specific implementation.
648+
649+
Args:
650+
table_name: The name of the table.
651+
schema_name: The name of the schema. Defaults to None.
652+
db_name: The name of the database. Defaults to None.
653+
delimiter: Generally: '.' for SQL names and '-' for Singer names.
654+
655+
Raises:
656+
ValueError: If all 3 name parts not supplied.
657+
658+
Returns:
659+
The fully qualified name as a string.
660+
"""
661+
parts = []
662+
dialect: SnowflakeDialect = self._engine.dialect
663+
664+
if db_name:
665+
parts.append(dialect.normalize_name(db_name))
666+
if schema_name:
667+
parts.append(dialect.normalize_name(schema_name))
668+
if table_name:
669+
parts.append(dialect.normalize_name(table_name))
670+
671+
if not parts:
672+
raise ValueError(
673+
"Could not generate fully qualified name: "
674+
+ ":".join(
675+
[
676+
db_name or "(unknown-db)",
677+
schema_name or "(unknown-schema)",
678+
table_name or "(unknown-table-name)",
679+
],
680+
),
681+
)
682+
683+
return delimiter.join(parts)

Diff for: tests/core.py

+57
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,61 @@ def setup(self) -> None:
458458
)
459459

460460

461+
class SnowflakeTargetExistingReservedNameTableAlter(TargetFileTestTemplate):
462+
name = "existing_reserved_name_table_alter"
463+
# This sends a schema that will request altering from TIMESTAMP_NTZ to VARCHAR
464+
465+
@property
466+
def singer_filepath(self) -> Path:
467+
current_dir = Path(__file__).resolve().parent
468+
return current_dir / "target_test_streams" / "reserved_words_in_table.singer"
469+
470+
def setup(self) -> None:
471+
connector = self.target.default_sink_class.connector_class(self.target.config)
472+
table = f'{self.target.config['database']}.{self.target.config['default_target_schema']}."ORDER"'.upper()
473+
connector.connection.execute(
474+
f"""
475+
CREATE OR REPLACE TABLE {table} (
476+
ID VARCHAR(16777216),
477+
COL_STR VARCHAR(16777216),
478+
COL_TS TIMESTAMP_NTZ(9),
479+
COL_INT STRING,
480+
COL_BOOL BOOLEAN,
481+
COL_VARIANT VARIANT,
482+
_SDC_BATCHED_AT TIMESTAMP_NTZ(9),
483+
_SDC_DELETED_AT VARCHAR(16777216),
484+
_SDC_EXTRACTED_AT TIMESTAMP_NTZ(9),
485+
_SDC_RECEIVED_AT TIMESTAMP_NTZ(9),
486+
_SDC_SEQUENCE NUMBER(38,0),
487+
_SDC_TABLE_VERSION NUMBER(38,0),
488+
PRIMARY KEY (ID)
489+
)
490+
""",
491+
)
492+
493+
494+
class SnowflakeTargetReservedWordsInTable(TargetFileTestTemplate):
495+
# Contains reserved words from
496+
# https://docs.snowflake.com/en/sql-reference/reserved-keywords
497+
# Syncs records then alters schema by adding a non-reserved word column.
498+
name = "reserved_words_in_table"
499+
500+
@property
501+
def singer_filepath(self) -> Path:
502+
current_dir = Path(__file__).resolve().parent
503+
return current_dir / "target_test_streams" / "reserved_words_in_table.singer"
504+
505+
def validate(self) -> None:
506+
connector = self.target.default_sink_class.connector_class(self.target.config)
507+
table = f'{self.target.config['database']}.{self.target.config['default_target_schema']}."ORDER"'.upper()
508+
result = connector.connection.execute(
509+
f"select * from {table}",
510+
)
511+
assert result.rowcount == 1
512+
row = result.first()
513+
assert len(row) == 13, f"Row has unexpected length {len(row)}"
514+
515+
461516
class SnowflakeTargetTypeEdgeCasesTest(TargetFileTestTemplate):
462517
name = "type_edge_cases"
463518

@@ -540,6 +595,8 @@ def singer_filepath(self) -> Path:
540595
SnowflakeTargetColonsInColName,
541596
SnowflakeTargetExistingTable,
542597
SnowflakeTargetExistingTableAlter,
598+
SnowflakeTargetExistingReservedNameTableAlter,
599+
SnowflakeTargetReservedWordsInTable,
543600
SnowflakeTargetTypeEdgeCasesTest,
544601
SnowflakeTargetColumnOrderMismatch,
545602
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{ "type": "SCHEMA", "stream": "ORDER", "schema": { "properties": { "id": { "type": [ "string", "null" ] }, "col_str": { "type": [ "string", "null" ] }, "col_ts": { "format": "date-time", "type": [ "string", "null" ] }, "col_int": { "type": "integer" }, "col_bool": { "type": [ "boolean", "null" ] }, "col_variant": {"type": "object"} }, "type": "object" }, "key_properties": [ "id" ], "bookmark_properties": [ "col_ts" ] }
2+
{ "type": "RECORD", "stream": "ORDER", "record": { "id": "123", "col_str": "foo", "col_ts": "2023-06-13 11:50:04.072", "col_int": 5, "col_bool": true, "col_variant": {"key": "val"} }, "time_extracted": "2023-06-14T18:08:23.074716+00:00" }

0 commit comments

Comments
 (0)