lsst-felis 28.2024.4500__py3-none-any.whl → 30.0.0rc3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- felis/__init__.py +9 -1
- felis/cli.py +308 -209
- felis/config/tap_schema/columns.csv +33 -0
- felis/config/tap_schema/key_columns.csv +8 -0
- felis/config/tap_schema/keys.csv +8 -0
- felis/config/tap_schema/schemas.csv +2 -0
- felis/config/tap_schema/tables.csv +6 -0
- felis/config/tap_schema/tap_schema_extensions.yaml +73 -0
- felis/datamodel.py +599 -59
- felis/db/{dialects.py → _dialects.py} +69 -4
- felis/db/{variants.py → _variants.py} +1 -1
- felis/db/database_context.py +917 -0
- felis/diff.py +234 -0
- felis/metadata.py +89 -19
- felis/tap_schema.py +271 -166
- felis/tests/postgresql.py +1 -1
- felis/tests/run_cli.py +79 -0
- felis/types.py +7 -7
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/METADATA +20 -16
- lsst_felis-30.0.0rc3.dist-info/RECORD +31 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/WHEEL +1 -1
- felis/db/utils.py +0 -409
- felis/tap.py +0 -597
- felis/tests/utils.py +0 -122
- felis/version.py +0 -2
- lsst_felis-28.2024.4500.dist-info/RECORD +0 -26
- felis/{schemas → config/tap_schema}/tap_schema_std.yaml +0 -0
- felis/db/{sqltypes.py → _sqltypes.py} +7 -7
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/entry_points.txt +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info/licenses}/COPYRIGHT +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info/licenses}/LICENSE +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/top_level.txt +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/zip-safe +0 -0
felis/tap.py
DELETED
|
@@ -1,597 +0,0 @@
|
|
|
1
|
-
"""Translate a Felis schema into a TAP_SCHEMA representation."""
|
|
2
|
-
|
|
3
|
-
# This file is part of felis.
|
|
4
|
-
#
|
|
5
|
-
# Developed for the LSST Data Management System.
|
|
6
|
-
# This product includes software developed by the LSST Project
|
|
7
|
-
# (https://www.lsst.org).
|
|
8
|
-
# See the COPYRIGHT file at the top-level directory of this distribution
|
|
9
|
-
# for details of code ownership.
|
|
10
|
-
#
|
|
11
|
-
# This program is free software: you can redistribute it and/or modify
|
|
12
|
-
# it under the terms of the GNU General Public License as published by
|
|
13
|
-
# the Free Software Foundation, either version 3 of the License, or
|
|
14
|
-
# (at your option) any later version.
|
|
15
|
-
#
|
|
16
|
-
# This program is distributed in the hope that it will be useful,
|
|
17
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
18
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
19
|
-
# GNU General Public License for more details.
|
|
20
|
-
#
|
|
21
|
-
# You should have received a copy of the GNU General Public License
|
|
22
|
-
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
23
|
-
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
|
-
import logging
|
|
27
|
-
import re
|
|
28
|
-
from collections.abc import Iterable, MutableMapping
|
|
29
|
-
from typing import Any
|
|
30
|
-
|
|
31
|
-
from sqlalchemy import Column, Integer, String
|
|
32
|
-
from sqlalchemy.engine import Engine
|
|
33
|
-
from sqlalchemy.engine.mock import MockConnection
|
|
34
|
-
from sqlalchemy.orm import Session, declarative_base, sessionmaker
|
|
35
|
-
from sqlalchemy.schema import MetaData
|
|
36
|
-
from sqlalchemy.sql.expression import Insert, insert
|
|
37
|
-
|
|
38
|
-
from felis import datamodel
|
|
39
|
-
|
|
40
|
-
from .datamodel import Constraint, ForeignKeyConstraint, Index, Schema, Table
|
|
41
|
-
from .types import FelisType
|
|
42
|
-
|
|
43
|
-
__all__ = ["TapLoadingVisitor", "init_tables"]
|
|
44
|
-
|
|
45
|
-
logger = logging.getLogger(__name__)
|
|
46
|
-
|
|
47
|
-
Tap11Base: Any = declarative_base() # Any to avoid mypy mess with SA 2
|
|
48
|
-
|
|
49
|
-
IDENTIFIER_LENGTH = 128
|
|
50
|
-
SMALL_FIELD_LENGTH = 32
|
|
51
|
-
SIMPLE_FIELD_LENGTH = 128
|
|
52
|
-
TEXT_FIELD_LENGTH = 2048
|
|
53
|
-
QUALIFIED_TABLE_LENGTH = 3 * IDENTIFIER_LENGTH + 2
|
|
54
|
-
|
|
55
|
-
_init_table_once = False
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def init_tables(
|
|
59
|
-
tap_schema_name: str | None = None,
|
|
60
|
-
tap_tables_postfix: str | None = None,
|
|
61
|
-
tap_schemas_table: str | None = None,
|
|
62
|
-
tap_tables_table: str | None = None,
|
|
63
|
-
tap_columns_table: str | None = None,
|
|
64
|
-
tap_keys_table: str | None = None,
|
|
65
|
-
tap_key_columns_table: str | None = None,
|
|
66
|
-
) -> MutableMapping[str, Any]:
|
|
67
|
-
"""Generate definitions for TAP tables.
|
|
68
|
-
|
|
69
|
-
Parameters
|
|
70
|
-
----------
|
|
71
|
-
tap_schema_name
|
|
72
|
-
Name of the TAP schema.
|
|
73
|
-
tap_tables_postfix
|
|
74
|
-
Postfix for table names.
|
|
75
|
-
tap_schemas_table
|
|
76
|
-
Name of the schemas table.
|
|
77
|
-
tap_tables_table
|
|
78
|
-
Name of the tables table.
|
|
79
|
-
tap_columns_table
|
|
80
|
-
Name of the columns table.
|
|
81
|
-
tap_keys_table
|
|
82
|
-
Name of the keys table.
|
|
83
|
-
tap_key_columns_table
|
|
84
|
-
Name of the key columns table.
|
|
85
|
-
|
|
86
|
-
Returns
|
|
87
|
-
-------
|
|
88
|
-
`dict` [ `str`, `Any`]
|
|
89
|
-
A dictionary of table definitions.
|
|
90
|
-
"""
|
|
91
|
-
postfix = tap_tables_postfix or ""
|
|
92
|
-
|
|
93
|
-
# Dirty hack to enable this method to be called more than once, replaces
|
|
94
|
-
# MetaData instance with a fresh copy if called more than once.
|
|
95
|
-
# TODO: probably replace ORM stuff with core sqlalchemy functions.
|
|
96
|
-
global _init_table_once
|
|
97
|
-
if not _init_table_once:
|
|
98
|
-
_init_table_once = True
|
|
99
|
-
else:
|
|
100
|
-
Tap11Base.metadata = MetaData()
|
|
101
|
-
|
|
102
|
-
if tap_schema_name:
|
|
103
|
-
Tap11Base.metadata.schema = tap_schema_name
|
|
104
|
-
|
|
105
|
-
class Tap11Schemas(Tap11Base):
|
|
106
|
-
__tablename__ = (tap_schemas_table or "schemas") + postfix
|
|
107
|
-
schema_name = Column(String(IDENTIFIER_LENGTH), primary_key=True, nullable=False)
|
|
108
|
-
utype = Column(String(SIMPLE_FIELD_LENGTH))
|
|
109
|
-
description = Column(String(TEXT_FIELD_LENGTH))
|
|
110
|
-
schema_index = Column(Integer)
|
|
111
|
-
|
|
112
|
-
class Tap11Tables(Tap11Base):
|
|
113
|
-
__tablename__ = (tap_tables_table or "tables") + postfix
|
|
114
|
-
schema_name = Column(String(IDENTIFIER_LENGTH), nullable=False)
|
|
115
|
-
table_name = Column(String(QUALIFIED_TABLE_LENGTH), nullable=False, primary_key=True)
|
|
116
|
-
table_type = Column(String(SMALL_FIELD_LENGTH), nullable=False)
|
|
117
|
-
utype = Column(String(SIMPLE_FIELD_LENGTH))
|
|
118
|
-
description = Column(String(TEXT_FIELD_LENGTH))
|
|
119
|
-
table_index = Column(Integer)
|
|
120
|
-
|
|
121
|
-
class Tap11Columns(Tap11Base):
|
|
122
|
-
__tablename__ = (tap_columns_table or "columns") + postfix
|
|
123
|
-
table_name = Column(String(QUALIFIED_TABLE_LENGTH), nullable=False, primary_key=True)
|
|
124
|
-
column_name = Column(String(IDENTIFIER_LENGTH), nullable=False, primary_key=True)
|
|
125
|
-
datatype = Column(String(SIMPLE_FIELD_LENGTH), nullable=False)
|
|
126
|
-
arraysize = Column(String(10))
|
|
127
|
-
xtype = Column(String(SIMPLE_FIELD_LENGTH))
|
|
128
|
-
# Size is deprecated
|
|
129
|
-
size = Column("size", Integer(), quote=True)
|
|
130
|
-
description = Column(String(TEXT_FIELD_LENGTH))
|
|
131
|
-
utype = Column(String(SIMPLE_FIELD_LENGTH))
|
|
132
|
-
unit = Column(String(SIMPLE_FIELD_LENGTH))
|
|
133
|
-
ucd = Column(String(SIMPLE_FIELD_LENGTH))
|
|
134
|
-
indexed = Column(Integer, nullable=False)
|
|
135
|
-
principal = Column(Integer, nullable=False)
|
|
136
|
-
std = Column(Integer, nullable=False)
|
|
137
|
-
column_index = Column(Integer)
|
|
138
|
-
|
|
139
|
-
class Tap11Keys(Tap11Base):
|
|
140
|
-
__tablename__ = (tap_keys_table or "keys") + postfix
|
|
141
|
-
key_id = Column(String(IDENTIFIER_LENGTH), nullable=False, primary_key=True)
|
|
142
|
-
from_table = Column(String(QUALIFIED_TABLE_LENGTH), nullable=False)
|
|
143
|
-
target_table = Column(String(QUALIFIED_TABLE_LENGTH), nullable=False)
|
|
144
|
-
description = Column(String(TEXT_FIELD_LENGTH))
|
|
145
|
-
utype = Column(String(SIMPLE_FIELD_LENGTH))
|
|
146
|
-
|
|
147
|
-
class Tap11KeyColumns(Tap11Base):
|
|
148
|
-
__tablename__ = (tap_key_columns_table or "key_columns") + postfix
|
|
149
|
-
key_id = Column(String(IDENTIFIER_LENGTH), nullable=False, primary_key=True)
|
|
150
|
-
from_column = Column(String(IDENTIFIER_LENGTH), nullable=False, primary_key=True)
|
|
151
|
-
target_column = Column(String(IDENTIFIER_LENGTH), nullable=False, primary_key=True)
|
|
152
|
-
|
|
153
|
-
return dict(
|
|
154
|
-
schemas=Tap11Schemas,
|
|
155
|
-
tables=Tap11Tables,
|
|
156
|
-
columns=Tap11Columns,
|
|
157
|
-
keys=Tap11Keys,
|
|
158
|
-
key_columns=Tap11KeyColumns,
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
class TapLoadingVisitor:
|
|
163
|
-
"""Generate TAP_SCHEMA data and insert it into a database using the
|
|
164
|
-
SQLAlchemy ORM.
|
|
165
|
-
|
|
166
|
-
Parameters
|
|
167
|
-
----------
|
|
168
|
-
engine
|
|
169
|
-
SQLAlchemy engine instance.
|
|
170
|
-
catalog_name
|
|
171
|
-
Name of the database catalog.
|
|
172
|
-
schema_name
|
|
173
|
-
Name of the schema.
|
|
174
|
-
tap_tables
|
|
175
|
-
Mapping of TAP_SCHEMA table name to its SQLAlchemy table object.
|
|
176
|
-
tap_schema_index
|
|
177
|
-
The index of the schema for this TAP environment.
|
|
178
|
-
"""
|
|
179
|
-
|
|
180
|
-
def __init__(
|
|
181
|
-
self,
|
|
182
|
-
engine: Engine | None,
|
|
183
|
-
catalog_name: str | None = None,
|
|
184
|
-
schema_name: str | None = None,
|
|
185
|
-
tap_tables: MutableMapping[str, Any] | None = None,
|
|
186
|
-
tap_schema_index: int | None = None,
|
|
187
|
-
) -> None:
|
|
188
|
-
"""Create a TAP loading visitor."""
|
|
189
|
-
self.graph_index: MutableMapping[str, Any] = {}
|
|
190
|
-
self.catalog_name = catalog_name
|
|
191
|
-
self.schema_name = schema_name
|
|
192
|
-
self.engine = engine
|
|
193
|
-
self._mock_connection: MockConnection | None = None
|
|
194
|
-
self.tables = tap_tables or init_tables()
|
|
195
|
-
self.tap_schema_index = tap_schema_index
|
|
196
|
-
|
|
197
|
-
@classmethod
|
|
198
|
-
def from_mock_connection(
|
|
199
|
-
cls,
|
|
200
|
-
mock_connection: MockConnection,
|
|
201
|
-
catalog_name: str | None = None,
|
|
202
|
-
schema_name: str | None = None,
|
|
203
|
-
tap_tables: MutableMapping[str, Any] | None = None,
|
|
204
|
-
tap_schema_index: int | None = None,
|
|
205
|
-
) -> TapLoadingVisitor:
|
|
206
|
-
"""Create a TAP visitor from a mock connection.
|
|
207
|
-
|
|
208
|
-
Parameters
|
|
209
|
-
----------
|
|
210
|
-
mock_connection
|
|
211
|
-
Mock connection object.
|
|
212
|
-
catalog_name
|
|
213
|
-
Name of the database catalog.
|
|
214
|
-
schema_name
|
|
215
|
-
Name of the database schema.
|
|
216
|
-
tap_tables
|
|
217
|
-
Optional mapping of table name to its SQLAlchemy table object.
|
|
218
|
-
tap_schema_index
|
|
219
|
-
The index of the schema for this TAP environment.
|
|
220
|
-
|
|
221
|
-
Returns
|
|
222
|
-
-------
|
|
223
|
-
`TapLoadingVisitor`
|
|
224
|
-
The TAP loading visitor.
|
|
225
|
-
"""
|
|
226
|
-
visitor = cls(engine=None, catalog_name=catalog_name, schema_name=schema_name, tap_tables=tap_tables)
|
|
227
|
-
visitor._mock_connection = mock_connection
|
|
228
|
-
visitor.tap_schema_index = tap_schema_index
|
|
229
|
-
return visitor
|
|
230
|
-
|
|
231
|
-
def visit_schema(self, schema_obj: Schema) -> None:
|
|
232
|
-
"""Visit a schema object and insert it into the TAP_SCHEMA database.
|
|
233
|
-
|
|
234
|
-
Parameters
|
|
235
|
-
----------
|
|
236
|
-
schema_obj
|
|
237
|
-
The schema object to visit.
|
|
238
|
-
"""
|
|
239
|
-
schema = self.tables["schemas"]()
|
|
240
|
-
# Override with default
|
|
241
|
-
self.schema_name = self.schema_name or schema_obj.name
|
|
242
|
-
|
|
243
|
-
schema.schema_name = self._schema_name()
|
|
244
|
-
schema.description = schema_obj.description
|
|
245
|
-
schema.utype = schema_obj.votable_utype
|
|
246
|
-
schema.schema_index = self.tap_schema_index
|
|
247
|
-
logger.debug(f"Set TAP_SCHEMA index: {self.tap_schema_index}")
|
|
248
|
-
|
|
249
|
-
if self.engine is not None:
|
|
250
|
-
session: Session = sessionmaker(self.engine)()
|
|
251
|
-
|
|
252
|
-
session.add(schema)
|
|
253
|
-
|
|
254
|
-
for table_obj in schema_obj.tables:
|
|
255
|
-
table, columns = self.visit_table(table_obj, schema_obj)
|
|
256
|
-
session.add(table)
|
|
257
|
-
session.add_all(columns)
|
|
258
|
-
|
|
259
|
-
keys, key_columns = self.visit_constraints(schema_obj)
|
|
260
|
-
session.add_all(keys)
|
|
261
|
-
session.add_all(key_columns)
|
|
262
|
-
|
|
263
|
-
logger.debug("Committing TAP schema: %s", schema_obj.name)
|
|
264
|
-
logger.debug("TAP tables: %s", len(self.tables))
|
|
265
|
-
session.commit()
|
|
266
|
-
else:
|
|
267
|
-
logger.info("Dry run, not inserting into database")
|
|
268
|
-
|
|
269
|
-
# Only if we are mocking (dry run)
|
|
270
|
-
assert self._mock_connection is not None, "Mock connection must not be None"
|
|
271
|
-
conn = self._mock_connection
|
|
272
|
-
conn.execute(_insert(self.tables["schemas"], schema))
|
|
273
|
-
|
|
274
|
-
for table_obj in schema_obj.tables:
|
|
275
|
-
table, columns = self.visit_table(table_obj, schema_obj)
|
|
276
|
-
conn.execute(_insert(self.tables["tables"], table))
|
|
277
|
-
for column in columns:
|
|
278
|
-
conn.execute(_insert(self.tables["columns"], column))
|
|
279
|
-
|
|
280
|
-
keys, key_columns = self.visit_constraints(schema_obj)
|
|
281
|
-
for key in keys:
|
|
282
|
-
conn.execute(_insert(self.tables["keys"], key))
|
|
283
|
-
for key_column in key_columns:
|
|
284
|
-
conn.execute(_insert(self.tables["key_columns"], key_column))
|
|
285
|
-
|
|
286
|
-
def visit_constraints(self, schema_obj: Schema) -> tuple:
|
|
287
|
-
"""Visit all constraints in a schema.
|
|
288
|
-
|
|
289
|
-
Parameters
|
|
290
|
-
----------
|
|
291
|
-
schema_obj
|
|
292
|
-
The schema object to visit.
|
|
293
|
-
|
|
294
|
-
Returns
|
|
295
|
-
-------
|
|
296
|
-
`tuple`
|
|
297
|
-
A tuple of all TAP_SCHEMA keys and key columns that were created.
|
|
298
|
-
"""
|
|
299
|
-
all_keys = []
|
|
300
|
-
all_key_columns = []
|
|
301
|
-
for table_obj in schema_obj.tables:
|
|
302
|
-
for c in table_obj.constraints:
|
|
303
|
-
key, key_columns = self.visit_constraint(c)
|
|
304
|
-
if not key:
|
|
305
|
-
continue
|
|
306
|
-
all_keys.append(key)
|
|
307
|
-
all_key_columns += key_columns
|
|
308
|
-
return all_keys, all_key_columns
|
|
309
|
-
|
|
310
|
-
def visit_table(self, table_obj: Table, schema_obj: Schema) -> tuple:
|
|
311
|
-
"""Visit a table object and build its TAP_SCHEMA representation.
|
|
312
|
-
|
|
313
|
-
Parameters
|
|
314
|
-
----------
|
|
315
|
-
table_obj
|
|
316
|
-
The table object to visit.
|
|
317
|
-
schema_obj
|
|
318
|
-
The schema object which the table belongs to.
|
|
319
|
-
|
|
320
|
-
Returns
|
|
321
|
-
-------
|
|
322
|
-
`tuple`
|
|
323
|
-
A tuple of the SQLAlchemy ORM objects for the tables and columns.
|
|
324
|
-
"""
|
|
325
|
-
table_id = table_obj.id
|
|
326
|
-
table = self.tables["tables"]()
|
|
327
|
-
table.schema_name = self._schema_name()
|
|
328
|
-
table.table_name = self._table_name(table_obj.name)
|
|
329
|
-
table.table_type = "table"
|
|
330
|
-
table.utype = table_obj.votable_utype
|
|
331
|
-
table.description = table_obj.description
|
|
332
|
-
table.table_index = 0 if table_obj.tap_table_index is None else table_obj.tap_table_index
|
|
333
|
-
|
|
334
|
-
columns = [self.visit_column(c, table_obj) for c in table_obj.columns]
|
|
335
|
-
self.visit_primary_key(table_obj.primary_key, table_obj)
|
|
336
|
-
|
|
337
|
-
for i in table_obj.indexes:
|
|
338
|
-
self.visit_index(i, table)
|
|
339
|
-
|
|
340
|
-
self.graph_index[table_id] = table
|
|
341
|
-
return table, columns
|
|
342
|
-
|
|
343
|
-
def check_column(self, column_obj: datamodel.Column) -> None:
|
|
344
|
-
"""Check consistency of VOTable attributes for a column.
|
|
345
|
-
|
|
346
|
-
Parameters
|
|
347
|
-
----------
|
|
348
|
-
column_obj
|
|
349
|
-
The column object to check.
|
|
350
|
-
|
|
351
|
-
Notes
|
|
352
|
-
-----
|
|
353
|
-
This method checks that a column with a sized datatype has either a
|
|
354
|
-
``votable:arraysize`` or a ``length`` attribute and issues a warning
|
|
355
|
-
message if not. It also checks if a column with a timestamp datatype
|
|
356
|
-
has a ``arraysize`` attribute and issues a warning if not.
|
|
357
|
-
"""
|
|
358
|
-
_id = column_obj.id
|
|
359
|
-
datatype_name = column_obj.datatype
|
|
360
|
-
felis_type = FelisType.felis_type(datatype_name.value)
|
|
361
|
-
if felis_type.is_sized:
|
|
362
|
-
# It is expected that both arraysize and length are fine for
|
|
363
|
-
# length types.
|
|
364
|
-
arraysize = column_obj.votable_arraysize or column_obj.length
|
|
365
|
-
if arraysize is None:
|
|
366
|
-
logger.warning(
|
|
367
|
-
f"votable:arraysize and length for {_id} are None for type {datatype_name}. "
|
|
368
|
-
'Using length "*". '
|
|
369
|
-
"Consider setting `votable:arraysize` or `length`."
|
|
370
|
-
)
|
|
371
|
-
if felis_type.is_timestamp:
|
|
372
|
-
# datetime types really should have a votable:arraysize, because
|
|
373
|
-
# they are converted to strings and the `length` is loosely to the
|
|
374
|
-
# string size
|
|
375
|
-
if not column_obj.votable_arraysize:
|
|
376
|
-
logger.warning(
|
|
377
|
-
f"votable:arraysize for {_id} is None for type {datatype_name}. "
|
|
378
|
-
f'Using length "*". '
|
|
379
|
-
"Consider setting `votable:arraysize` to an appropriate size for "
|
|
380
|
-
"materialized datetime/timestamp strings."
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
def visit_column(self, column_obj: datamodel.Column, table_obj: Table) -> Tap11Base:
|
|
384
|
-
"""Visit a column object and build its TAP_SCHEMA representation.
|
|
385
|
-
|
|
386
|
-
Parameters
|
|
387
|
-
----------
|
|
388
|
-
column_obj
|
|
389
|
-
The column object to visit.
|
|
390
|
-
table_obj
|
|
391
|
-
The table object which the column belongs to.
|
|
392
|
-
|
|
393
|
-
Returns
|
|
394
|
-
-------
|
|
395
|
-
``Tap11Base``
|
|
396
|
-
The SQLAlchemy ORM object for the column.
|
|
397
|
-
"""
|
|
398
|
-
self.check_column(column_obj)
|
|
399
|
-
column_id = column_obj.id
|
|
400
|
-
table_name = self._table_name(table_obj.name)
|
|
401
|
-
|
|
402
|
-
column = self.tables["columns"]()
|
|
403
|
-
column.table_name = table_name
|
|
404
|
-
column.column_name = column_obj.name
|
|
405
|
-
|
|
406
|
-
felis_datatype = column_obj.datatype
|
|
407
|
-
felis_type = FelisType.felis_type(felis_datatype.value)
|
|
408
|
-
column.datatype = column_obj.votable_datatype or felis_type.votable_name
|
|
409
|
-
|
|
410
|
-
column.arraysize = column_obj.votable_arraysize
|
|
411
|
-
|
|
412
|
-
def _is_int(s: str) -> bool:
|
|
413
|
-
try:
|
|
414
|
-
int(s)
|
|
415
|
-
return True
|
|
416
|
-
except ValueError:
|
|
417
|
-
return False
|
|
418
|
-
|
|
419
|
-
# Handle the deprecated size attribute
|
|
420
|
-
arraysize = column.arraysize
|
|
421
|
-
if arraysize is not None and arraysize != "":
|
|
422
|
-
if isinstance(arraysize, int):
|
|
423
|
-
column.size = arraysize
|
|
424
|
-
elif _is_int(arraysize):
|
|
425
|
-
column.size = int(arraysize)
|
|
426
|
-
elif bool(re.match(r"^[0-9]+\*$", arraysize)):
|
|
427
|
-
column.size = int(arraysize.replace("*", ""))
|
|
428
|
-
|
|
429
|
-
if column.size is not None:
|
|
430
|
-
logger.debug(f"Set size to {column.size} for {column.column_name} with arraysize {arraysize}")
|
|
431
|
-
|
|
432
|
-
column.xtype = column_obj.votable_xtype
|
|
433
|
-
column.description = column_obj.description
|
|
434
|
-
column.utype = column_obj.votable_utype
|
|
435
|
-
|
|
436
|
-
unit = column_obj.ivoa_unit or column_obj.fits_tunit
|
|
437
|
-
column.unit = unit
|
|
438
|
-
column.ucd = column_obj.ivoa_ucd
|
|
439
|
-
|
|
440
|
-
# We modify this after we process columns
|
|
441
|
-
column.indexed = 0
|
|
442
|
-
|
|
443
|
-
column.principal = column_obj.tap_principal
|
|
444
|
-
column.std = column_obj.tap_std
|
|
445
|
-
column.column_index = column_obj.tap_column_index
|
|
446
|
-
|
|
447
|
-
self.graph_index[column_id] = column
|
|
448
|
-
return column
|
|
449
|
-
|
|
450
|
-
def visit_primary_key(self, primary_key_obj: str | Iterable[str] | None, table_obj: Table) -> None:
|
|
451
|
-
"""Visit a primary key object and update the TAP_SCHEMA representation.
|
|
452
|
-
|
|
453
|
-
Parameters
|
|
454
|
-
----------
|
|
455
|
-
primary_key_obj
|
|
456
|
-
The primary key object to visit.
|
|
457
|
-
table_obj
|
|
458
|
-
The table object which the primary key belongs to.
|
|
459
|
-
"""
|
|
460
|
-
if primary_key_obj:
|
|
461
|
-
if isinstance(primary_key_obj, str):
|
|
462
|
-
primary_key_obj = [primary_key_obj]
|
|
463
|
-
columns = [self.graph_index[c_id] for c_id in primary_key_obj]
|
|
464
|
-
# if just one column and it's indexed, update the object
|
|
465
|
-
if len(columns) == 1:
|
|
466
|
-
columns[0].indexed = 1
|
|
467
|
-
|
|
468
|
-
def visit_constraint(self, constraint_obj: Constraint) -> tuple:
|
|
469
|
-
"""Visit a constraint object and build its TAP_SCHEMA representation.
|
|
470
|
-
|
|
471
|
-
Parameters
|
|
472
|
-
----------
|
|
473
|
-
constraint_obj
|
|
474
|
-
The constraint object to visit.
|
|
475
|
-
|
|
476
|
-
Returns
|
|
477
|
-
-------
|
|
478
|
-
`tuple`
|
|
479
|
-
A tuple of the SQLAlchemy ORM objects for the TAP_SCHEMA ``key``
|
|
480
|
-
and ``key_columns`` data.
|
|
481
|
-
"""
|
|
482
|
-
key = None
|
|
483
|
-
key_columns = []
|
|
484
|
-
if isinstance(constraint_obj, ForeignKeyConstraint):
|
|
485
|
-
constraint_name = constraint_obj.name
|
|
486
|
-
description = constraint_obj.description
|
|
487
|
-
utype = constraint_obj.votable_utype
|
|
488
|
-
|
|
489
|
-
columns = [self.graph_index[col_id] for col_id in getattr(constraint_obj, "columns", [])]
|
|
490
|
-
refcolumns = [
|
|
491
|
-
self.graph_index[refcol_id] for refcol_id in getattr(constraint_obj, "referenced_columns", [])
|
|
492
|
-
]
|
|
493
|
-
|
|
494
|
-
table_name = None
|
|
495
|
-
for column in columns:
|
|
496
|
-
if not table_name:
|
|
497
|
-
table_name = column.table_name
|
|
498
|
-
if table_name != column.table_name:
|
|
499
|
-
raise ValueError("Inconsisent use of table names")
|
|
500
|
-
|
|
501
|
-
table_name = None
|
|
502
|
-
for column in refcolumns:
|
|
503
|
-
if not table_name:
|
|
504
|
-
table_name = column.table_name
|
|
505
|
-
if table_name != column.table_name:
|
|
506
|
-
raise ValueError("Inconsisent use of table names")
|
|
507
|
-
first_column = columns[0]
|
|
508
|
-
first_refcolumn = refcolumns[0]
|
|
509
|
-
|
|
510
|
-
key = self.tables["keys"]()
|
|
511
|
-
key.key_id = constraint_name
|
|
512
|
-
key.from_table = first_column.table_name
|
|
513
|
-
key.target_table = first_refcolumn.table_name
|
|
514
|
-
key.description = description
|
|
515
|
-
key.utype = utype
|
|
516
|
-
for column, refcolumn in zip(columns, refcolumns):
|
|
517
|
-
key_column = self.tables["key_columns"]()
|
|
518
|
-
key_column.key_id = constraint_name
|
|
519
|
-
key_column.from_column = column.column_name
|
|
520
|
-
key_column.target_column = refcolumn.column_name
|
|
521
|
-
key_columns.append(key_column)
|
|
522
|
-
return key, key_columns
|
|
523
|
-
|
|
524
|
-
def visit_index(self, index_obj: Index, table_obj: Table) -> None:
|
|
525
|
-
"""Visit an index object and update the TAP_SCHEMA representation.
|
|
526
|
-
|
|
527
|
-
Parameters
|
|
528
|
-
----------
|
|
529
|
-
index_obj
|
|
530
|
-
The index object to visit.
|
|
531
|
-
table_obj
|
|
532
|
-
The table object which the index belongs to.
|
|
533
|
-
"""
|
|
534
|
-
columns = [self.graph_index[col_id] for col_id in getattr(index_obj, "columns", [])]
|
|
535
|
-
# if just one column and it's indexed, update the object
|
|
536
|
-
if len(columns) == 1:
|
|
537
|
-
columns[0].indexed = 1
|
|
538
|
-
return None
|
|
539
|
-
|
|
540
|
-
def _schema_name(
|
|
541
|
-
self, schema_name: str | None = None
|
|
542
|
-
) -> str | None: # DM-44870: Usage of this method needs to be better understood and possibly removed
|
|
543
|
-
"""Return the schema name.
|
|
544
|
-
|
|
545
|
-
Parameters
|
|
546
|
-
----------
|
|
547
|
-
schema_name
|
|
548
|
-
Name of the schema.
|
|
549
|
-
|
|
550
|
-
Returns
|
|
551
|
-
-------
|
|
552
|
-
schema_name
|
|
553
|
-
The schema name.
|
|
554
|
-
"""
|
|
555
|
-
# If _schema_name is None, SQLAlchemy will catch it
|
|
556
|
-
_schema_name = schema_name or self.schema_name
|
|
557
|
-
if self.catalog_name and _schema_name:
|
|
558
|
-
return ".".join([self.catalog_name, _schema_name])
|
|
559
|
-
return _schema_name
|
|
560
|
-
|
|
561
|
-
def _table_name(self, table_name: str) -> str:
|
|
562
|
-
"""Return the table name.
|
|
563
|
-
|
|
564
|
-
Parameters
|
|
565
|
-
----------
|
|
566
|
-
table_name
|
|
567
|
-
Name of the table.
|
|
568
|
-
"""
|
|
569
|
-
schema_name = self._schema_name()
|
|
570
|
-
if schema_name:
|
|
571
|
-
return ".".join([schema_name, table_name])
|
|
572
|
-
return table_name
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
def _insert(table: Tap11Base, value: Any) -> Insert:
|
|
576
|
-
"""Return a SQLAlchemy insert statement.
|
|
577
|
-
|
|
578
|
-
Parameters
|
|
579
|
-
----------
|
|
580
|
-
table
|
|
581
|
-
The table we are inserting into.
|
|
582
|
-
value
|
|
583
|
-
An object representing the object we are inserting to the table.
|
|
584
|
-
|
|
585
|
-
Returns
|
|
586
|
-
-------
|
|
587
|
-
`Insert`
|
|
588
|
-
SQLAlchemy insert statement.
|
|
589
|
-
"""
|
|
590
|
-
values_dict = {}
|
|
591
|
-
for i in table.__table__.columns:
|
|
592
|
-
name = i.name
|
|
593
|
-
column_value = getattr(value, i.name)
|
|
594
|
-
if isinstance(column_value, str):
|
|
595
|
-
column_value = column_value.replace("'", "''")
|
|
596
|
-
values_dict[name] = column_value
|
|
597
|
-
return insert(table).values(values_dict)
|