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.
Files changed (33) hide show
  1. felis/__init__.py +9 -1
  2. felis/cli.py +308 -209
  3. felis/config/tap_schema/columns.csv +33 -0
  4. felis/config/tap_schema/key_columns.csv +8 -0
  5. felis/config/tap_schema/keys.csv +8 -0
  6. felis/config/tap_schema/schemas.csv +2 -0
  7. felis/config/tap_schema/tables.csv +6 -0
  8. felis/config/tap_schema/tap_schema_extensions.yaml +73 -0
  9. felis/datamodel.py +599 -59
  10. felis/db/{dialects.py → _dialects.py} +69 -4
  11. felis/db/{variants.py → _variants.py} +1 -1
  12. felis/db/database_context.py +917 -0
  13. felis/diff.py +234 -0
  14. felis/metadata.py +89 -19
  15. felis/tap_schema.py +271 -166
  16. felis/tests/postgresql.py +1 -1
  17. felis/tests/run_cli.py +79 -0
  18. felis/types.py +7 -7
  19. {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/METADATA +20 -16
  20. lsst_felis-30.0.0rc3.dist-info/RECORD +31 -0
  21. {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/WHEEL +1 -1
  22. felis/db/utils.py +0 -409
  23. felis/tap.py +0 -597
  24. felis/tests/utils.py +0 -122
  25. felis/version.py +0 -2
  26. lsst_felis-28.2024.4500.dist-info/RECORD +0 -26
  27. felis/{schemas → config/tap_schema}/tap_schema_std.yaml +0 -0
  28. felis/db/{sqltypes.py → _sqltypes.py} +7 -7
  29. {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/entry_points.txt +0 -0
  30. {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info/licenses}/COPYRIGHT +0 -0
  31. {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info/licenses}/LICENSE +0 -0
  32. {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/top_level.txt +0 -0
  33. {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)