|
42 | 42 | GET_RELATIONS_MACRO_NAME = "redshift__get_relations" |
43 | 43 | SHOW_TABLES_FROM_SCHEMA_MACRO_NAME = "redshift__show_tables_from_schema" |
44 | 44 |
|
| 45 | +# Redshift type OID -> SQL data type name, as reported by both the legacy path's |
| 46 | +# information_schema.columns.data_type and SHOW COLUMNS. Used to describe temp relations |
| 47 | +# from the driver's cursor description (see get_columns_in_temp_relation); the names have |
| 48 | +# to match whichever of those two paths described the target relation, or schema comparison |
| 49 | +# reports a type change on every run. Verified against SHOW COLUMNS on Redshift 1.0.358853 |
| 50 | +# -- note bpchar reports as "character", and varbyte as "binary varying". |
| 51 | +TYPE_OID_TO_DATA_TYPE: Dict[int, str] = { |
| 52 | + 16: "boolean", # bool |
| 53 | + 20: "bigint", # int8 |
| 54 | + 21: "smallint", # int2 |
| 55 | + 23: "integer", # int4 |
| 56 | + 25: "character varying", # text |
| 57 | + 700: "real", # float4 |
| 58 | + 701: "double precision", # float8 |
| 59 | + 1042: "character", # bpchar |
| 60 | + 1043: "character varying", # varchar |
| 61 | + 1082: "date", # date |
| 62 | + 1083: "time without time zone", # time |
| 63 | + 1114: "timestamp without time zone", # timestamp |
| 64 | + 1184: "timestamp with time zone", # timestamptz |
| 65 | + 1188: "interval year to month", # intervaly2m |
| 66 | + 1190: "interval day to second", # intervald2s |
| 67 | + 1266: "time with time zone", # timetz |
| 68 | + 1700: "numeric", # numeric |
| 69 | + 2935: "hllsketch", # hllsketch |
| 70 | + 3000: "geometry", # geometry |
| 71 | + 3001: "geography", # geography |
| 72 | + 4000: "super", # super |
| 73 | + 6551: "binary varying", # varbyte |
| 74 | +} |
| 75 | + |
| 76 | +# information_schema reports its internal type name where SHOW COLUMNS reports the SQL name, |
| 77 | +# so a fallback column only compares equal if it follows whichever path described the target. |
| 78 | +# Everything else in the map above agrees across both; these are the exceptions, verified in |
| 79 | +# tests/functional/test_type_oid_mapping.py. |
| 80 | +TYPE_OID_TO_INFORMATION_SCHEMA_DATA_TYPE: Dict[int, str] = { |
| 81 | + 1188: "intervaly2m", # SHOW COLUMNS: interval year to month |
| 82 | + 1190: "intervald2s", # SHOW COLUMNS: interval day to second |
| 83 | +} |
| 84 | + |
45 | 85 | REDSHIFT_SKIP_AUTOCOMMIT_TRANSACTION_STATEMENTS = BehaviorFlag( |
46 | 86 | name="redshift_skip_autocommit_transaction_statements", |
47 | 87 | default=False, |
@@ -199,6 +239,111 @@ def use_show_apis(self) -> bool: |
199 | 239 | """ |
200 | 240 | return bool(self.config.credentials.datasharing) |
201 | 241 |
|
| 242 | + @available |
| 243 | + def get_columns_in_temp_relation(self, relation: BaseRelation) -> List[Any]: |
| 244 | + """Describe a temporary relation from the driver instead of the catalog. |
| 245 | +
|
| 246 | + Needed when the connection's database is a datashare consumer database: temp |
| 247 | + relations stay queryable but are invisible to ``information_schema.columns``, |
| 248 | + ``pg_attribute`` and ``svv_columns`` alike, and ``SHOW COLUMNS`` cannot address |
| 249 | + them because they carry no database or schema. |
| 250 | +
|
| 251 | + Sizes come from ``cursor.ps["row_desc"]``'s ``type_modifier``, not from |
| 252 | + ``cursor.description``: ``redshift_connector`` hardcodes the PEP 249 |
| 253 | + size/precision/scale fields to ``None`` (see its ``Cursor._getDescription``). |
| 254 | + Without real sizes, every sized column looks changed against the target on the |
| 255 | + next run and gets needlessly rewritten. |
| 256 | + """ |
| 257 | + sql = f"select * from {self.quote(relation.identifier)} limit 0" |
| 258 | + _, cursor = self.connections.add_select_query(sql) |
| 259 | + |
| 260 | + # `cursor.ps["row_desc"]` is an internal, undocumented structure of |
| 261 | + # `redshift_connector`, not a public API -- there is no supported alternative for |
| 262 | + # reaching `type_modifier`. `_getDescription` builds `cursor.description` from this |
| 263 | + # same list, in the same order, so index-aligned lookup here is safe. |
| 264 | + try: |
| 265 | + row_descriptions = cursor.ps["row_desc"] |
| 266 | + except (AttributeError, KeyError, TypeError): |
| 267 | + row_descriptions = [] |
| 268 | + |
| 269 | + columns = [] |
| 270 | + for i, description in enumerate(cursor.description or []): |
| 271 | + # PEP 249: (name, type_code, display_size, internal_size, precision, scale, null_ok) |
| 272 | + column_name = description[0] |
| 273 | + type_code = description[1] |
| 274 | + |
| 275 | + type_modifier = None |
| 276 | + if i < len(row_descriptions): |
| 277 | + type_modifier = row_descriptions[i].get("type_modifier") |
| 278 | + |
| 279 | + data_type = self._temp_relation_data_type(type_code) |
| 280 | + |
| 281 | + # Only string and exact-numeric types feed size into Column.data_type, which is |
| 282 | + # what schema comparison comes down to. For every other type the size fields are |
| 283 | + # ignored, so leave them unset. |
| 284 | + char_size = None |
| 285 | + numeric_precision = None |
| 286 | + numeric_scale = None |
| 287 | + if type_modifier is not None and type_modifier >= 0: |
| 288 | + # Postgres wire-protocol convention: VARHDRSZ (4 bytes) is added to both a |
| 289 | + # string's declared length and a numeric's packed (precision, scale) pair. |
| 290 | + # -1 means "no modifier" (unconstrained/default size) and is left unset. |
| 291 | + raw_modifier = type_modifier - 4 |
| 292 | + if data_type in ("character varying", "character"): |
| 293 | + char_size = raw_modifier if raw_modifier > 0 else None |
| 294 | + elif data_type == "numeric": |
| 295 | + numeric_precision = raw_modifier >> 16 |
| 296 | + numeric_scale = raw_modifier & 0xFFFF |
| 297 | + |
| 298 | + columns.append( |
| 299 | + self.Column( |
| 300 | + column=column_name, |
| 301 | + dtype=data_type, |
| 302 | + char_size=char_size, |
| 303 | + numeric_precision=numeric_precision, |
| 304 | + numeric_scale=numeric_scale, |
| 305 | + ) |
| 306 | + ) |
| 307 | + |
| 308 | + return columns |
| 309 | + |
| 310 | + def _temp_relation_data_type(self, type_code: Any) -> str: |
| 311 | + """Map a driver type code onto the name the target relation's describer reports. |
| 312 | +
|
| 313 | + The target goes through SHOW COLUMNS when ``datasharing`` is on and through |
| 314 | + information_schema when it is off, and the two disagree on a few type names. |
| 315 | + """ |
| 316 | + if not self.use_show_apis(): |
| 317 | + data_type = TYPE_OID_TO_INFORMATION_SCHEMA_DATA_TYPE.get(type_code) |
| 318 | + if data_type is not None: |
| 319 | + return data_type |
| 320 | + |
| 321 | + data_type = TYPE_OID_TO_DATA_TYPE.get(type_code) |
| 322 | + if data_type is not None: |
| 323 | + return data_type |
| 324 | + |
| 325 | + # Unmapped OID. The driver's own label is closer than nothing, but its lookup is an |
| 326 | + # IntEnum call that raises for codes it doesn't recognise either -- and a column |
| 327 | + # whose type cannot be named at all is better reported than described with an |
| 328 | + # invented name, which would only fail later as invalid DDL. |
| 329 | + try: |
| 330 | + fallback = str(self.connections.data_type_code_to_name(type_code)).lower() |
| 331 | + except Exception as exc: |
| 332 | + raise dbt_common.exceptions.DbtRuntimeError( |
| 333 | + f"Cannot describe temporary relation column: Redshift returned type code " |
| 334 | + f"{type_code}, which neither dbt-redshift nor redshift_connector " |
| 335 | + f"recognises. Please report this at " |
| 336 | + f"https://github.com/dbt-labs/dbt-adapters/issues" |
| 337 | + ) from exc |
| 338 | + |
| 339 | + logger.debug( |
| 340 | + f"No known Redshift data type for type code {type_code}; " |
| 341 | + f"falling back to {fallback!r}. Schema comparison for this column may be " |
| 342 | + f"inaccurate -- please report this at " |
| 343 | + f"https://github.com/dbt-labs/dbt-adapters/issues" |
| 344 | + ) |
| 345 | + return fallback |
| 346 | + |
202 | 347 | @available |
203 | 348 | def drop_without_cascade(self) -> bool: |
204 | 349 | """Whether to omit CASCADE from DROP statements. |
|
0 commit comments