Skip to content

Commit 88f526f

Browse files
committed
Types: Trace an unresolved column under its own type name
Looking an array up through its element type logged the element's name, which belongs to no column, so a trace of what the map lacks named types nobody can add. The lookup is separate from the resolution that records it, and records the name the column has, once. `UnresolvedType` reaches the front door, so a caller can tell a column with no representation from one it can render, in place of skipping whole tables.
1 parent ce84cf2 commit 88f526f

5 files changed

Lines changed: 78 additions & 40 deletions

File tree

CHANGES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
- Reflection: A column whose CrateDB type the dialect cannot represent now
1414
raises `CompileError` naming that type, the table and the column when asked
1515
to compile it, where it previously raised `AttributeError` from inside
16-
SQLAlchemy and named none of them. Reading such a column, and printing its
17-
type, are unaffected
16+
SQLAlchemy and named none of them. Such a column still reads, and from
17+
SQLAlchemy 1.4 onwards printing its type yields the CrateDB type name
1818
- Reflection: Added `geo_point` and `geo_shape` to the reflected type map,
1919
resolving to the `Geopoint` and `Geoshape` types the package already offers,
2020
which lets tables such as `sys.summits` round-trip into DDL

src/sqlalchemy_cratedb/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# software solely pursuant to the terms of the relevant commercial agreement.
2121

2222
from .compat.api13 import monkeypatch_add_exec_driver_sql
23-
from .dialect import dialect
23+
from .dialect import UnresolvedType, dialect
2424
from .predicate import match
2525
from .sa_version import SA_1_4, SA_VERSION
2626
from .support import insert_bulk
@@ -74,6 +74,7 @@
7474
Geoshape,
7575
ObjectArray,
7676
ObjectType,
77+
UnresolvedType,
7778
match,
7879
knn_match,
7980
insert_bulk,

src/sqlalchemy_cratedb/compiler.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,9 @@ def visit_unresolved(self, type_, **kw):
296296
"""
297297
Refuse a CrateDB type reflection could not resolve, naming it.
298298
299-
Only compiling the type has no answer. Reading the column works, and so
300-
does printing its type, which SQLAlchemy's string compiler renders from
301-
the type itself without coming through here.
299+
Compiling the type is what has no answer; reading the column works. From
300+
SQLAlchemy 1.4 onwards, printing the type reaches the string compiler,
301+
which renders it from the type itself without coming through here.
302302
"""
303303
raise sa.exc.CompileError(
304304
"Unable to represent CrateDB type '{0}' in SQLAlchemy".format(type_.type_name)

src/sqlalchemy_cratedb/dialect.py

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
from sqlalchemy.engine import default, reflection
2828
from sqlalchemy.exc import SQLAlchemyError
2929
from sqlalchemy.sql import functions
30-
from sqlalchemy.types import ARRAY
3130
from sqlalchemy.util import asbool, to_list
3231

3332
from .compiler import (
@@ -48,9 +47,10 @@ class UnresolvedType(sqltypes.UserDefinedType):
4847
A CrateDB type the dialect has no SQLAlchemy counterpart for.
4948
5049
Reflection yields it for a column it cannot resolve, carrying the CrateDB
51-
type name as reported. Reading such a column is unaffected, and so is
52-
printing its type, which shows the name. Compiling it as SQL is what has no
53-
answer, and `CrateTypeCompiler` refuses there, naming the type.
50+
type name as reported. Such a column still reads. Compiling the type as SQL
51+
is what has no answer, and `CrateTypeCompiler` refuses there, naming the
52+
type. From SQLAlchemy 1.4 onwards, printing the type yields the name; older
53+
versions route printing through the same compiler and raise.
5454
"""
5555

5656
__visit_name__ = "unresolved"
@@ -109,7 +109,7 @@ class Double(sqltypes.Float):
109109

110110
TYPES_MAP["double"] = DOUBLE
111111
TYPES_MAP["double precision"] = DOUBLE_PRECISION
112-
except Exception: # noqa: S110
112+
except ImportError:
113113
pass
114114

115115

@@ -517,16 +517,31 @@ def _resolve_type(self, type_):
517517
Turn a type name, as `information_schema.columns` reports it, into the
518518
SQLAlchemy type a reflected column of that type carries.
519519
520-
The map answers first, so that `object_array` keeps the entry it has.
521-
Every other name ending in `_array` names an array of the type the rest
522-
of the name names, and resolves to an `ARRAY` of that type. Two shapes
523-
have no representation and are left unresolved: an array whose element
524-
type is itself unresolved, and an array of arrays, which SQLAlchemy's
525-
`ARRAY` cannot hold and `CrateTypeCompiler` refuses to render.
520+
A name with no type behind it yields an `UnresolvedType` under that same
521+
name, so an unresolved array reports the array's name rather than its
522+
element's, and the log carries the name the column actually has.
523+
"""
524+
resolved = self._lookup_type(type_)
525+
if resolved is None:
526+
# Reflecting `pg_catalog` alone leaves twenty-odd columns unresolved,
527+
# so this records the name for tracing rather than to raise an alarm
528+
# about a schema nobody asked to reflect. What does deserve one is
529+
# asking such a column for SQL, and that raises.
530+
log.debug("Unable to resolve CrateDB type: %s", type_)
531+
return UnresolvedType(type_)
532+
return resolved
533+
534+
def _lookup_type(self, type_):
535+
"""
536+
Find the SQLAlchemy type for a CrateDB type name, or `None` for a name
537+
the dialect has nothing to offer for.
526538
527-
Whatever is left unresolved keeps the name of the type the column
528-
actually has, so an unresolved array reports the array's name rather
529-
than its element's.
539+
The map answers first, so that `object_array` resolves to its own entry.
540+
Every other name ending in `_array` names an array of the type the rest
541+
of the name names, and yields an `ARRAY` of that type. Two shapes have no
542+
representation: an array whose element type is itself unresolved, and an
543+
array of arrays, which SQLAlchemy's `ARRAY` cannot hold and
544+
`CrateTypeCompiler` refuses to render.
530545
531546
An `ARRAY` built here carries its element type into DDL and into the
532547
reflected column. It converts no values, because SQLAlchemy's `ARRAY`
@@ -536,17 +551,15 @@ def _resolve_type(self, type_):
536551
resolved = TYPES_MAP.get(type_)
537552
if resolved is not None:
538553
return resolved
539-
element_name = type_[: -len(ARRAY_SUFFIX)] if type_.endswith(ARRAY_SUFFIX) else ""
540-
if element_name and not element_name.endswith(ARRAY_SUFFIX):
541-
element_type = self._resolve_type(element_name)
542-
if not isinstance(element_type, UnresolvedType):
543-
return ARRAY(element_type)
544-
# Reflecting `pg_catalog` alone leaves twenty-odd columns unresolved, so
545-
# this records the name for tracing rather than to raise an alarm about
546-
# a schema nobody asked to reflect. What does deserve one is asking such
547-
# a column for SQL, and that raises.
548-
log.debug("Unable to resolve CrateDB type: %s", type_)
549-
return UnresolvedType(type_)
554+
if not type_.endswith(ARRAY_SUFFIX):
555+
return None
556+
element_name = type_[: -len(ARRAY_SUFFIX)]
557+
if not element_name or element_name.endswith(ARRAY_SUFFIX):
558+
return None
559+
element_type = self._lookup_type(element_name)
560+
if element_type is None:
561+
return None
562+
return sqltypes.ARRAY(element_type)
550563

551564
def has_ilike_operator(self):
552565
"""

tests/reflection_test.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,9 @@
2626
from sqlalchemy import types as sqltypes
2727

2828
from sqlalchemy_cratedb import Geopoint, Geoshape, ObjectArray
29-
from sqlalchemy_cratedb.dialect import TYPES_MAP, CrateDialect
29+
from sqlalchemy_cratedb.dialect import ARRAY_SUFFIX, TYPES_MAP, CrateDialect
3030
from sqlalchemy_cratedb.sa_version import SA_1_4, SA_2_0, SA_VERSION
3131

32-
# CrateDB names an array type after the type it holds.
33-
ARRAY_SUFFIX = "_array"
34-
3532
# CrateDB reports `regproc` for the `pg_catalog` columns holding a reference to
3633
# a function. No table can declare one, so it has no DDL spelling to map to.
3734
UNRESOLVABLE = "regproc"
@@ -129,10 +126,14 @@ def test_double_precision_array_renders_as_an_array_of_its_element_type():
129126
assert render_type("double precision_array") == expected
130127

131128

132-
def test_every_mapped_type_has_an_array_form():
129+
def test_every_mapped_type_is_the_element_of_its_array_form():
133130
"""
134131
A type gains its array form by being mapped, rather than by being listed a
135132
second time under its array name.
133+
134+
This pins which type the array holds. Whether that array renders is the
135+
element type's business: `float_vector` needs a dimension that reflection
136+
cannot recover, and refuses in its array form exactly as it does alone.
136137
"""
137138
dialect = CrateDialect()
138139
derived = {
@@ -147,6 +148,16 @@ def test_every_mapped_type_has_an_array_form():
147148
assert type(resolved.item_type) is type(sqltypes.to_instance(element_type))
148149

149150

151+
def test_reflected_numeric_array_refuses_ddl_without_a_precision():
152+
"""
153+
Reflection recovers a type from its name alone, and an array of `NUMERIC` is
154+
no more storable without a precision than the scalar is.
155+
"""
156+
with pytest.raises(sa.exc.CompileError) as ex:
157+
render_ddl("numeric_array")
158+
assert "CrateDB stores a NUMERIC column only with a precision" in str(ex.value)
159+
160+
150161
def test_reflected_object_array_keeps_its_own_type():
151162
"""
152163
An array of `OBJECT` tracks mutation of the objects it holds, which an
@@ -209,13 +220,26 @@ def test_unresolved_type_remains_selectable():
209220
assert "SELECT t.c" in str(table.select().compile(dialect=dialect))
210221

211222

212-
def test_unresolved_type_is_logged(caplog):
223+
@pytest.mark.parametrize(
224+
"data_type",
225+
[UNRESOLVABLE, "{0}{1}".format(UNRESOLVABLE, ARRAY_SUFFIX)],
226+
)
227+
def test_unresolved_type_is_logged_under_the_column_type_name(caplog, data_type):
213228
"""
214-
Reflecting a type the dialect does not know leaves a record of its name.
229+
Reflecting a type the dialect does not know leaves one record, naming the
230+
type the column has.
231+
232+
An array is looked up through its element type, whose name belongs to no
233+
column, and so belongs in no trace of what the map is missing.
215234
"""
216235
with caplog.at_level(logging.DEBUG, logger="sqlalchemy_cratedb.dialect"):
217-
CrateDialect()._resolve_type(UNRESOLVABLE)
218-
assert UNRESOLVABLE in caplog.text
236+
CrateDialect()._resolve_type(data_type)
237+
messages = [
238+
record.getMessage()
239+
for record in caplog.records
240+
if record.name == "sqlalchemy_cratedb.dialect"
241+
]
242+
assert messages == ["Unable to resolve CrateDB type: {0}".format(data_type)]
219243

220244

221245
@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3")

0 commit comments

Comments
 (0)