pyoq-sql 1.0.2__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.
- pyoq/__init__.py +10 -0
- pyoq/__main__.py +5 -0
- pyoq/_native.pyi +5 -0
- pyoq/cli/__init__.py +5 -0
- pyoq/cli/commands.py +270 -0
- pyoq/cli/defaults.py +98 -0
- pyoq/cli/services.py +97 -0
- pyoq/config/__init__.py +31 -0
- pyoq/config/connection.py +161 -0
- pyoq/config/loader.py +289 -0
- pyoq/config/models.py +245 -0
- pyoq/config/values.py +142 -0
- pyoq/descriptors.py +165 -0
- pyoq/diagnostics/__init__.py +68 -0
- pyoq/diagnostics/budget.py +136 -0
- pyoq/diagnostics/events.py +137 -0
- pyoq/diagnostics/fingerprint.py +267 -0
- pyoq/diagnostics/instrumented.py +237 -0
- pyoq/diagnostics/metrics.py +61 -0
- pyoq/diagnostics/observation.py +227 -0
- pyoq/diagnostics/scoped.py +103 -0
- pyoq/django/__init__.py +15 -0
- pyoq/django/apps.py +17 -0
- pyoq/django/execution.py +317 -0
- pyoq/django/generation.py +59 -0
- pyoq/django/management/__init__.py +0 -0
- pyoq/django/management/commands/__init__.py +0 -0
- pyoq/django/management/commands/makemigrations.py +53 -0
- pyoq/django/management/commands/pyoq_codegen.py +75 -0
- pyoq/django/parameters.py +101 -0
- pyoq/django/schema.py +379 -0
- pyoq/django/settings.py +87 -0
- pyoq/django/timeouts.py +105 -0
- pyoq/dsl/__init__.py +64 -0
- pyoq/dsl/aio/__init__.py +31 -0
- pyoq/dsl/aio/context.py +295 -0
- pyoq/dsl/aio/queries.py +335 -0
- pyoq/dsl/aio/writes.py +368 -0
- pyoq/dsl/context.py +326 -0
- pyoq/dsl/entry.py +37 -0
- pyoq/dsl/labels.py +36 -0
- pyoq/dsl/queries.py +339 -0
- pyoq/dsl/result.py +164 -0
- pyoq/dsl/writes.py +360 -0
- pyoq/errors.py +317 -0
- pyoq/fastapi/__init__.py +32 -0
- pyoq/fastapi/dependencies.py +167 -0
- pyoq/fastapi/lifespan.py +119 -0
- pyoq/fetching/__init__.py +55 -0
- pyoq/fetching/collections.py +136 -0
- pyoq/fetching/execution.py +587 -0
- pyoq/fetching/joined.py +79 -0
- pyoq/fetching/nesting.py +183 -0
- pyoq/fetching/plans.py +541 -0
- pyoq/fetching/select_in.py +149 -0
- pyoq/fetching/tables.py +110 -0
- pyoq/generation/__init__.py +54 -0
- pyoq/generation/cleanup.py +44 -0
- pyoq/generation/contracts.py +248 -0
- pyoq/generation/drift.py +169 -0
- pyoq/generation/lock.py +33 -0
- pyoq/generation/manifest.py +114 -0
- pyoq/generation/model.py +1001 -0
- pyoq/generation/pipeline.py +119 -0
- pyoq/generation/rendering/__init__.py +5 -0
- pyoq/generation/rendering/domains.py +51 -0
- pyoq/generation/rendering/enums.py +29 -0
- pyoq/generation/rendering/exports.py +70 -0
- pyoq/generation/rendering/imports.py +63 -0
- pyoq/generation/rendering/package.py +56 -0
- pyoq/generation/rendering/relations.py +133 -0
- pyoq/generation/rendering/routines.py +396 -0
- pyoq/generation/rendering/rows.py +79 -0
- pyoq/generation/rendering/source.py +121 -0
- pyoq/generation/rendering/tables.py +300 -0
- pyoq/generation/rendering/writes.py +514 -0
- pyoq/generation/validation.py +27 -0
- pyoq/generation/writer.py +184 -0
- pyoq/hydration/__init__.py +24 -0
- pyoq/hydration/engine.py +155 -0
- pyoq/hydration/identity.py +194 -0
- pyoq/hydration/plan.py +116 -0
- pyoq/migrations/__init__.py +9 -0
- pyoq/migrations/alembic.py +106 -0
- pyoq/migrations/hooks.py +75 -0
- pyoq/naming.py +261 -0
- pyoq/policies/__init__.py +47 -0
- pyoq/policies/bypass.py +122 -0
- pyoq/policies/governed.py +430 -0
- pyoq/policies/model.py +242 -0
- pyoq/policies/rewriting.py +263 -0
- pyoq/py.typed +1 -0
- pyoq/query/__init__.py +312 -0
- pyoq/query/aggregates.py +172 -0
- pyoq/query/arrays.py +65 -0
- pyoq/query/binding.py +52 -0
- pyoq/query/capabilities.py +317 -0
- pyoq/query/casts.py +73 -0
- pyoq/query/choices.py +185 -0
- pyoq/query/decoding.py +360 -0
- pyoq/query/documents.py +56 -0
- pyoq/query/execution/__init__.py +63 -0
- pyoq/query/execution/aio/__init__.py +31 -0
- pyoq/query/execution/aio/operations.py +228 -0
- pyoq/query/execution/aio/pooling.py +233 -0
- pyoq/query/execution/aio/streaming.py +161 -0
- pyoq/query/execution/aio/transactions.py +105 -0
- pyoq/query/execution/batch.py +96 -0
- pyoq/query/execution/binding_style.py +30 -0
- pyoq/query/execution/compilation.py +48 -0
- pyoq/query/execution/context.py +61 -0
- pyoq/query/execution/control.py +50 -0
- pyoq/query/execution/operations.py +224 -0
- pyoq/query/execution/planning.py +107 -0
- pyoq/query/execution/pooling.py +279 -0
- pyoq/query/execution/results.py +36 -0
- pyoq/query/execution/streaming.py +178 -0
- pyoq/query/execution/transactions.py +95 -0
- pyoq/query/expressions.py +1200 -0
- pyoq/query/fields.py +60 -0
- pyoq/query/mysql/__init__.py +59 -0
- pyoq/query/mysql/aio/__init__.py +38 -0
- pyoq/query/mysql/aio/commands.py +389 -0
- pyoq/query/mysql/aio/driver.py +196 -0
- pyoq/query/mysql/aio/executor.py +123 -0
- pyoq/query/mysql/aio/factory.py +26 -0
- pyoq/query/mysql/aio/operations.py +38 -0
- pyoq/query/mysql/aio/pool.py +53 -0
- pyoq/query/mysql/aio/transactions.py +313 -0
- pyoq/query/mysql/commands.py +354 -0
- pyoq/query/mysql/compiler.py +134 -0
- pyoq/query/mysql/context.py +20 -0
- pyoq/query/mysql/executor.py +126 -0
- pyoq/query/mysql/expressions.py +244 -0
- pyoq/query/mysql/factory.py +46 -0
- pyoq/query/mysql/health.py +66 -0
- pyoq/query/mysql/identifiers.py +9 -0
- pyoq/query/mysql/model.py +79 -0
- pyoq/query/mysql/operations.py +43 -0
- pyoq/query/mysql/parameters.py +69 -0
- pyoq/query/mysql/planning.py +20 -0
- pyoq/query/mysql/pool.py +67 -0
- pyoq/query/mysql/transactions.py +331 -0
- pyoq/query/mysql/writes.py +73 -0
- pyoq/query/nodes.py +750 -0
- pyoq/query/postgres/__init__.py +48 -0
- pyoq/query/postgres/aio/__init__.py +25 -0
- pyoq/query/postgres/aio/bulk.py +56 -0
- pyoq/query/postgres/aio/commands.py +264 -0
- pyoq/query/postgres/aio/executor.py +152 -0
- pyoq/query/postgres/aio/factory.py +26 -0
- pyoq/query/postgres/aio/operations.py +26 -0
- pyoq/query/postgres/aio/pool.py +40 -0
- pyoq/query/postgres/aio/transactions.py +295 -0
- pyoq/query/postgres/bulk.py +62 -0
- pyoq/query/postgres/commands.py +238 -0
- pyoq/query/postgres/compiler.py +114 -0
- pyoq/query/postgres/context.py +20 -0
- pyoq/query/postgres/executor.py +147 -0
- pyoq/query/postgres/expressions.py +311 -0
- pyoq/query/postgres/factory.py +24 -0
- pyoq/query/postgres/health.py +24 -0
- pyoq/query/postgres/identifiers.py +9 -0
- pyoq/query/postgres/model.py +81 -0
- pyoq/query/postgres/operations.py +25 -0
- pyoq/query/postgres/parameters.py +71 -0
- pyoq/query/postgres/planning.py +20 -0
- pyoq/query/postgres/pool.py +52 -0
- pyoq/query/postgres/transactions.py +295 -0
- pyoq/query/postgres/writes.py +37 -0
- pyoq/query/projections.py +105 -0
- pyoq/query/raw.py +90 -0
- pyoq/query/recursion.py +265 -0
- pyoq/query/rendering/__init__.py +1 -0
- pyoq/query/rendering/expressions.py +913 -0
- pyoq/query/rendering/identifiers.py +40 -0
- pyoq/query/rendering/projections.py +63 -0
- pyoq/query/rendering/queries.py +334 -0
- pyoq/query/rendering/sources.py +66 -0
- pyoq/query/rendering/writes.py +176 -0
- pyoq/query/results.py +459 -0
- pyoq/query/routines.py +196 -0
- pyoq/query/rows.py +156 -0
- pyoq/query/select.py +793 -0
- pyoq/query/select_nodes.py +277 -0
- pyoq/query/sources.py +236 -0
- pyoq/query/sqlite/__init__.py +43 -0
- pyoq/query/sqlite/commands.py +201 -0
- pyoq/query/sqlite/compiler.py +139 -0
- pyoq/query/sqlite/context.py +20 -0
- pyoq/query/sqlite/executor.py +119 -0
- pyoq/query/sqlite/expressions.py +224 -0
- pyoq/query/sqlite/factory.py +32 -0
- pyoq/query/sqlite/health.py +28 -0
- pyoq/query/sqlite/identifiers.py +9 -0
- pyoq/query/sqlite/model.py +73 -0
- pyoq/query/sqlite/operations.py +36 -0
- pyoq/query/sqlite/parameters.py +50 -0
- pyoq/query/sqlite/planning.py +20 -0
- pyoq/query/sqlite/pool.py +50 -0
- pyoq/query/sqlite/streaming.py +13 -0
- pyoq/query/sqlite/transactions.py +274 -0
- pyoq/query/sqlite/writes.py +35 -0
- pyoq/query/statements.py +27 -0
- pyoq/query/values.py +23 -0
- pyoq/query/vendor.py +162 -0
- pyoq/query/windows.py +424 -0
- pyoq/query/write_nodes.py +174 -0
- pyoq/query/writes.py +628 -0
- pyoq/relations/__init__.py +66 -0
- pyoq/relations/batching.py +219 -0
- pyoq/relations/derivation.py +111 -0
- pyoq/relations/fetching.py +355 -0
- pyoq/relations/graph.py +245 -0
- pyoq/relations/loading.py +74 -0
- pyoq/relations/model.py +75 -0
- pyoq/relations/planning.py +206 -0
- pyoq/runtime/__init__.py +9 -0
- pyoq/runtime/kernels.py +25 -0
- pyoq/runtime/python.py +43 -0
- pyoq/runtime/selection.py +73 -0
- pyoq/sanic/__init__.py +32 -0
- pyoq/sanic/scope.py +197 -0
- pyoq/sanic/workers.py +129 -0
- pyoq/schema/__init__.py +108 -0
- pyoq/schema/codec.py +711 -0
- pyoq/schema/models.py +604 -0
- pyoq/schema/mysql/__init__.py +16 -0
- pyoq/schema/mysql/connection.py +73 -0
- pyoq/schema/mysql/dsn.py +72 -0
- pyoq/schema/mysql/records.py +354 -0
- pyoq/schema/mysql/reflection.py +309 -0
- pyoq/schema/mysql/source.py +30 -0
- pyoq/schema/mysql/sql.py +128 -0
- pyoq/schema/mysql/types.py +105 -0
- pyoq/schema/postgres/__init__.py +13 -0
- pyoq/schema/postgres/connection.py +63 -0
- pyoq/schema/postgres/records.py +384 -0
- pyoq/schema/postgres/reflection.py +466 -0
- pyoq/schema/postgres/source.py +30 -0
- pyoq/schema/postgres/sql.py +246 -0
- pyoq/schema/postgres/types.py +98 -0
- pyoq/schema/registry.py +45 -0
- pyoq/schema/source.py +15 -0
- pyoq/schema/sqlite/__init__.py +6 -0
- pyoq/schema/sqlite/connection.py +54 -0
- pyoq/schema/sqlite/records.py +167 -0
- pyoq/schema/sqlite/reflection.py +393 -0
- pyoq/schema/sqlite/source.py +30 -0
- pyoq/schema/sqlite/sql.py +254 -0
- pyoq/schema/sqlite/types.py +74 -0
- pyoq/serving/__init__.py +23 -0
- pyoq/serving/databases.py +107 -0
- pyoq/serving/opening.py +331 -0
- pyoq/snapshots/__init__.py +20 -0
- pyoq/snapshots/drift.py +312 -0
- pyoq/snapshots/files.py +96 -0
- pyoq/snapshots/routing.py +40 -0
- pyoq/snapshots/source.py +33 -0
- pyoq/tracing/__init__.py +5 -0
- pyoq/tracing/spans.py +89 -0
- pyoq/unset.py +14 -0
- pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
- pyoq_sql-1.0.2.dist-info/RECORD +267 -0
- pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
- pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""SQLite metadata reflection into the immutable schema model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from pyoq.config import DatabaseDialect
|
|
10
|
+
from pyoq.errors import SchemaInspectionError
|
|
11
|
+
from pyoq.schema import (
|
|
12
|
+
Capability,
|
|
13
|
+
CapabilitySet,
|
|
14
|
+
Catalog,
|
|
15
|
+
CheckConstraint,
|
|
16
|
+
Column,
|
|
17
|
+
GeneratedKind,
|
|
18
|
+
GeneratedValue,
|
|
19
|
+
Identifier,
|
|
20
|
+
Index,
|
|
21
|
+
IndexTerm,
|
|
22
|
+
Key,
|
|
23
|
+
KeyKind,
|
|
24
|
+
MetadataEntry,
|
|
25
|
+
ObjectReference,
|
|
26
|
+
ReferentialAction,
|
|
27
|
+
Relation,
|
|
28
|
+
Schema,
|
|
29
|
+
SchemaSnapshot,
|
|
30
|
+
SqlExpression,
|
|
31
|
+
Table,
|
|
32
|
+
View,
|
|
33
|
+
)
|
|
34
|
+
from pyoq.schema.sqlite.records import (
|
|
35
|
+
ColumnRecord,
|
|
36
|
+
ForeignKeyRecord,
|
|
37
|
+
IndexRecord,
|
|
38
|
+
IndexTermRecord,
|
|
39
|
+
SchemaObjectRecord,
|
|
40
|
+
column,
|
|
41
|
+
foreign_key,
|
|
42
|
+
index,
|
|
43
|
+
index_term,
|
|
44
|
+
schema_object,
|
|
45
|
+
)
|
|
46
|
+
from pyoq.schema.sqlite.sql import (
|
|
47
|
+
ParsedIndexTerm,
|
|
48
|
+
check_constraints,
|
|
49
|
+
descending_primary_columns,
|
|
50
|
+
generated_columns,
|
|
51
|
+
index_parts,
|
|
52
|
+
table_options,
|
|
53
|
+
view_query,
|
|
54
|
+
)
|
|
55
|
+
from pyoq.schema.sqlite.types import sqlite_type
|
|
56
|
+
|
|
57
|
+
_CAPABILITIES = CapabilitySet(
|
|
58
|
+
frozenset(
|
|
59
|
+
{
|
|
60
|
+
Capability.CHECK_CONSTRAINTS,
|
|
61
|
+
Capability.EXPRESSION_INDEXES,
|
|
62
|
+
Capability.GENERATED_COLUMNS,
|
|
63
|
+
Capability.PARTIAL_INDEXES,
|
|
64
|
+
Capability.VIEWS,
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
_SCHEMA_OBJECTS = """
|
|
69
|
+
SELECT type, name, sql
|
|
70
|
+
FROM sqlite_schema
|
|
71
|
+
WHERE type IN ('index', 'table', 'view') AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'
|
|
72
|
+
ORDER BY type, name
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True, slots=True)
|
|
77
|
+
class SQLiteSchemaInspector:
|
|
78
|
+
def inspect(self, connection: sqlite3.Connection) -> SchemaSnapshot:
|
|
79
|
+
try:
|
|
80
|
+
objects = tuple(
|
|
81
|
+
schema_object(row) for row in connection.execute(_SCHEMA_OBJECTS)
|
|
82
|
+
)
|
|
83
|
+
object_map = {record.name: record for record in objects}
|
|
84
|
+
table_records = tuple(
|
|
85
|
+
record for record in objects if record.kind == "table"
|
|
86
|
+
)
|
|
87
|
+
tables = tuple(
|
|
88
|
+
self._table(connection, record, object_map) for record in table_records
|
|
89
|
+
)
|
|
90
|
+
views = tuple(
|
|
91
|
+
self._view(connection, record)
|
|
92
|
+
for record in objects
|
|
93
|
+
if record.kind == "view"
|
|
94
|
+
)
|
|
95
|
+
except sqlite3.Error as error:
|
|
96
|
+
message = f"unable to inspect SQLite schema: {error}"
|
|
97
|
+
raise SchemaInspectionError(message) from error
|
|
98
|
+
return SchemaSnapshot(
|
|
99
|
+
DatabaseDialect.SQLITE,
|
|
100
|
+
(Catalog(None, (Schema(None, tables, views),)),),
|
|
101
|
+
_CAPABILITIES,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _table(
|
|
105
|
+
self,
|
|
106
|
+
connection: sqlite3.Connection,
|
|
107
|
+
record: SchemaObjectRecord,
|
|
108
|
+
objects: dict[str, SchemaObjectRecord],
|
|
109
|
+
) -> Table:
|
|
110
|
+
column_records = _columns(connection, record.name)
|
|
111
|
+
columns = _model_columns(column_records, record.sql)
|
|
112
|
+
index_records = _indexes(connection, record.name)
|
|
113
|
+
keys = _keys(connection, column_records, index_records)
|
|
114
|
+
relations = _relations(connection, record.name, objects)
|
|
115
|
+
indexes = _model_indexes(connection, index_records, objects)
|
|
116
|
+
return Table(
|
|
117
|
+
Identifier(record.name),
|
|
118
|
+
columns,
|
|
119
|
+
keys=keys,
|
|
120
|
+
relations=relations,
|
|
121
|
+
indexes=indexes,
|
|
122
|
+
checks=_model_checks(record.sql),
|
|
123
|
+
metadata=_table_metadata(record.sql),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _view(
|
|
127
|
+
self,
|
|
128
|
+
connection: sqlite3.Connection,
|
|
129
|
+
record: SchemaObjectRecord,
|
|
130
|
+
) -> View:
|
|
131
|
+
columns = _model_columns(_columns(connection, record.name), None)
|
|
132
|
+
query = view_query(record.sql)
|
|
133
|
+
return View(
|
|
134
|
+
Identifier(record.name),
|
|
135
|
+
columns,
|
|
136
|
+
None if query is None else SqlExpression(query),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _columns(connection: sqlite3.Connection, name: str) -> tuple[ColumnRecord, ...]:
|
|
141
|
+
rows = connection.execute(
|
|
142
|
+
'SELECT cid, name, type, "notnull", dflt_value, pk, hidden '
|
|
143
|
+
"FROM pragma_table_xinfo(?) ORDER BY cid",
|
|
144
|
+
(name,),
|
|
145
|
+
)
|
|
146
|
+
return tuple(record for row in rows if (record := column(row)).hidden != 1)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _indexes(connection: sqlite3.Connection, name: str) -> tuple[IndexRecord, ...]:
|
|
150
|
+
rows = connection.execute(
|
|
151
|
+
'SELECT seq, name, "unique", origin, partial '
|
|
152
|
+
"FROM pragma_index_list(?) ORDER BY seq",
|
|
153
|
+
(name,),
|
|
154
|
+
)
|
|
155
|
+
return tuple(index(row) for row in rows)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _index_terms(
|
|
159
|
+
connection: sqlite3.Connection,
|
|
160
|
+
name: str,
|
|
161
|
+
) -> tuple[IndexTermRecord, ...]:
|
|
162
|
+
rows = connection.execute(
|
|
163
|
+
"SELECT seqno, cid, name, desc, coll, key "
|
|
164
|
+
"FROM pragma_index_xinfo(?) ORDER BY seqno",
|
|
165
|
+
(name,),
|
|
166
|
+
)
|
|
167
|
+
return tuple(term for row in rows if (term := index_term(row)).key_term)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _model_columns(
|
|
171
|
+
records: tuple[ColumnRecord, ...],
|
|
172
|
+
create_sql: str | None,
|
|
173
|
+
) -> tuple[Column, ...]:
|
|
174
|
+
generated = generated_columns(create_sql)
|
|
175
|
+
descending_primary = descending_primary_columns(create_sql)
|
|
176
|
+
single_primary = sum(record.primary_ordinal > 0 for record in records) == 1
|
|
177
|
+
without_rowid, _ = table_options(create_sql)
|
|
178
|
+
return tuple(
|
|
179
|
+
_model_column(
|
|
180
|
+
record,
|
|
181
|
+
generated.get(record.name),
|
|
182
|
+
single_primary,
|
|
183
|
+
without_rowid,
|
|
184
|
+
record.name in descending_primary,
|
|
185
|
+
)
|
|
186
|
+
for record in records
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _model_column(
|
|
191
|
+
record: ColumnRecord,
|
|
192
|
+
generated: tuple[str, bool] | None,
|
|
193
|
+
single_primary: bool,
|
|
194
|
+
without_rowid: bool,
|
|
195
|
+
descending_primary: bool,
|
|
196
|
+
) -> Column:
|
|
197
|
+
identity = (
|
|
198
|
+
record.primary_ordinal > 0
|
|
199
|
+
and single_primary
|
|
200
|
+
and not without_rowid
|
|
201
|
+
and not descending_primary
|
|
202
|
+
and record.declared_type.strip().upper() == "INTEGER"
|
|
203
|
+
)
|
|
204
|
+
generated_value = _generated_value(generated, identity)
|
|
205
|
+
return Column(
|
|
206
|
+
Identifier(record.name),
|
|
207
|
+
sqlite_type(record.declared_type),
|
|
208
|
+
nullable=not record.not_null and not identity,
|
|
209
|
+
default=None if record.default is None else SqlExpression(record.default),
|
|
210
|
+
generated=generated_value,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _generated_value(
|
|
215
|
+
generated: tuple[str, bool] | None,
|
|
216
|
+
identity: bool,
|
|
217
|
+
) -> GeneratedValue | None:
|
|
218
|
+
if generated is not None:
|
|
219
|
+
expression, stored = generated
|
|
220
|
+
kind = (
|
|
221
|
+
GeneratedKind.COMPUTED_STORED if stored else GeneratedKind.COMPUTED_VIRTUAL
|
|
222
|
+
)
|
|
223
|
+
return GeneratedValue(kind, SqlExpression(expression))
|
|
224
|
+
if identity:
|
|
225
|
+
return GeneratedValue(GeneratedKind.IDENTITY_BY_DEFAULT)
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _keys(
|
|
230
|
+
connection: sqlite3.Connection,
|
|
231
|
+
columns: tuple[ColumnRecord, ...],
|
|
232
|
+
indexes: tuple[IndexRecord, ...],
|
|
233
|
+
) -> tuple[Key, ...]:
|
|
234
|
+
primary_columns = tuple(
|
|
235
|
+
Identifier(record.name)
|
|
236
|
+
for record in sorted(columns, key=lambda candidate: candidate.primary_ordinal)
|
|
237
|
+
if record.primary_ordinal > 0
|
|
238
|
+
)
|
|
239
|
+
keys: list[Key] = []
|
|
240
|
+
if primary_columns:
|
|
241
|
+
keys.append(Key(KeyKind.PRIMARY, primary_columns))
|
|
242
|
+
for record in indexes:
|
|
243
|
+
if record.origin != "u":
|
|
244
|
+
continue
|
|
245
|
+
terms = _index_terms(connection, record.name)
|
|
246
|
+
names = tuple(term.column_name for term in terms)
|
|
247
|
+
if all(name is not None for name in names):
|
|
248
|
+
keys.append(
|
|
249
|
+
Key(
|
|
250
|
+
KeyKind.UNIQUE,
|
|
251
|
+
tuple(Identifier(name) for name in names if name is not None),
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
return tuple(keys)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _relations(
|
|
258
|
+
connection: sqlite3.Connection,
|
|
259
|
+
table_name: str,
|
|
260
|
+
objects: dict[str, SchemaObjectRecord],
|
|
261
|
+
) -> tuple[Relation, ...]:
|
|
262
|
+
rows = connection.execute(
|
|
263
|
+
'SELECT id, seq, "table", "from", "to", on_update, on_delete, match '
|
|
264
|
+
"FROM pragma_foreign_key_list(?) ORDER BY id, seq",
|
|
265
|
+
(table_name,),
|
|
266
|
+
)
|
|
267
|
+
grouped: dict[int, list[ForeignKeyRecord]] = defaultdict(list)
|
|
268
|
+
for row in rows:
|
|
269
|
+
record = foreign_key(row)
|
|
270
|
+
grouped[record.relation_id].append(record)
|
|
271
|
+
return tuple(
|
|
272
|
+
_relation(connection, tuple(grouped[relation_id]), objects)
|
|
273
|
+
for relation_id in sorted(grouped)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _relation(
|
|
278
|
+
connection: sqlite3.Connection,
|
|
279
|
+
records: tuple[ForeignKeyRecord, ...],
|
|
280
|
+
objects: dict[str, SchemaObjectRecord],
|
|
281
|
+
) -> Relation:
|
|
282
|
+
first = records[0]
|
|
283
|
+
target_columns = tuple(record.target_column for record in records)
|
|
284
|
+
if any(name is None for name in target_columns):
|
|
285
|
+
target_columns = _target_primary_columns(
|
|
286
|
+
connection, first.target_table, objects
|
|
287
|
+
)
|
|
288
|
+
return Relation(
|
|
289
|
+
tuple(Identifier(record.source_column) for record in records),
|
|
290
|
+
ObjectReference(Identifier(first.target_table)),
|
|
291
|
+
tuple(Identifier(name) for name in target_columns if name is not None),
|
|
292
|
+
on_update=_referential_action(first.on_update),
|
|
293
|
+
on_delete=_referential_action(first.on_delete),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _target_primary_columns(
|
|
298
|
+
connection: sqlite3.Connection,
|
|
299
|
+
table_name: str,
|
|
300
|
+
objects: dict[str, SchemaObjectRecord],
|
|
301
|
+
) -> tuple[str, ...]:
|
|
302
|
+
if table_name not in objects:
|
|
303
|
+
message = f"foreign key references unavailable table: {table_name}"
|
|
304
|
+
raise SchemaInspectionError(message)
|
|
305
|
+
records = sorted(
|
|
306
|
+
_columns(connection, table_name), key=lambda item: item.primary_ordinal
|
|
307
|
+
)
|
|
308
|
+
names = tuple(record.name for record in records if record.primary_ordinal > 0)
|
|
309
|
+
if not names:
|
|
310
|
+
message = f"foreign key target has no primary key: {table_name}"
|
|
311
|
+
raise SchemaInspectionError(message)
|
|
312
|
+
return names
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _model_indexes(
|
|
316
|
+
connection: sqlite3.Connection,
|
|
317
|
+
records: tuple[IndexRecord, ...],
|
|
318
|
+
objects: dict[str, SchemaObjectRecord],
|
|
319
|
+
) -> tuple[Index, ...]:
|
|
320
|
+
indexes: list[Index] = []
|
|
321
|
+
for record in records:
|
|
322
|
+
if record.origin != "c":
|
|
323
|
+
continue
|
|
324
|
+
schema_record = objects.get(record.name)
|
|
325
|
+
indexes.append(_model_index(connection, record, schema_record))
|
|
326
|
+
return tuple(indexes)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _model_index(
|
|
330
|
+
connection: sqlite3.Connection,
|
|
331
|
+
record: IndexRecord,
|
|
332
|
+
schema_record: SchemaObjectRecord | None,
|
|
333
|
+
) -> Index:
|
|
334
|
+
metadata_terms = _index_terms(connection, record.name)
|
|
335
|
+
sql = None if schema_record is None else schema_record.sql
|
|
336
|
+
parsed_terms, predicate = ((), None) if sql is None else index_parts(sql)
|
|
337
|
+
terms = tuple(
|
|
338
|
+
_model_index_term(
|
|
339
|
+
term, parsed_terms[index] if index < len(parsed_terms) else None
|
|
340
|
+
)
|
|
341
|
+
for index, term in enumerate(metadata_terms)
|
|
342
|
+
)
|
|
343
|
+
return Index(
|
|
344
|
+
Identifier(record.name),
|
|
345
|
+
terms,
|
|
346
|
+
unique=record.unique,
|
|
347
|
+
predicate=None if predicate is None else SqlExpression(predicate),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _model_index_term(
|
|
352
|
+
record: IndexTermRecord,
|
|
353
|
+
parsed: ParsedIndexTerm | None,
|
|
354
|
+
) -> IndexTerm:
|
|
355
|
+
if record.column_name is not None:
|
|
356
|
+
return IndexTerm(Identifier(record.column_name), descending=record.descending)
|
|
357
|
+
if parsed is None:
|
|
358
|
+
message = "unable to recover SQLite expression index term"
|
|
359
|
+
raise SchemaInspectionError(message)
|
|
360
|
+
return IndexTerm(
|
|
361
|
+
expression=SqlExpression(parsed.expression),
|
|
362
|
+
descending=parsed.descending,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _referential_action(value: str) -> ReferentialAction:
|
|
367
|
+
normalized = value.lower().replace(" ", "-")
|
|
368
|
+
try:
|
|
369
|
+
return ReferentialAction(normalized)
|
|
370
|
+
except ValueError as error:
|
|
371
|
+
message = f"unsupported SQLite referential action: {value}"
|
|
372
|
+
raise SchemaInspectionError(message) from error
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _model_checks(create_sql: str | None) -> tuple[CheckConstraint, ...]:
|
|
376
|
+
return tuple(
|
|
377
|
+
CheckConstraint(
|
|
378
|
+
SqlExpression(check.expression),
|
|
379
|
+
None if check.name is None else Identifier(check.name),
|
|
380
|
+
)
|
|
381
|
+
for check in check_constraints(create_sql)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _table_metadata(create_sql: str | None) -> tuple[MetadataEntry, ...]:
|
|
386
|
+
without_rowid, strict = table_options(create_sql)
|
|
387
|
+
return (
|
|
388
|
+
MetadataEntry("strict", str(strict).lower()),
|
|
389
|
+
MetadataEntry("without-rowid", str(without_rowid).lower()),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
__all__ = ("SQLiteSchemaInspector",)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Configuration-backed SQLite schema source."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from pyoq.config import Configuration, DatabaseDialect
|
|
8
|
+
from pyoq.errors import OperationUnavailableError
|
|
9
|
+
from pyoq.schema import SchemaSnapshot
|
|
10
|
+
from pyoq.schema.sqlite.connection import SQLiteConnectionProvider
|
|
11
|
+
from pyoq.schema.sqlite.reflection import SQLiteSchemaInspector
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class SQLiteSchemaSource:
|
|
16
|
+
connections: SQLiteConnectionProvider = field(
|
|
17
|
+
default_factory=SQLiteConnectionProvider
|
|
18
|
+
)
|
|
19
|
+
inspector: SQLiteSchemaInspector = field(default_factory=SQLiteSchemaInspector)
|
|
20
|
+
|
|
21
|
+
def load(self, configuration: Configuration) -> SchemaSnapshot:
|
|
22
|
+
dialect = configuration.active_profile.dialect
|
|
23
|
+
if dialect is not DatabaseDialect.SQLITE:
|
|
24
|
+
message = f"schema inspection is not available for {dialect.value}"
|
|
25
|
+
raise OperationUnavailableError(message)
|
|
26
|
+
with self.connections.open(configuration) as connection:
|
|
27
|
+
return self.inspector.inspect(connection)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ("SQLiteSchemaSource",)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Small SQLite DDL extraction helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from pyoq.errors import SchemaInspectionError
|
|
9
|
+
|
|
10
|
+
_IDENTIFIER = re.compile(
|
|
11
|
+
r"\s*(?:\"((?:\"\"|[^\"])*)\"|`((?:``|[^`])*)`|\[([^]]*)\]|([^\s,()]+))"
|
|
12
|
+
)
|
|
13
|
+
_GENERATED = re.compile(r"\b(?:GENERATED\s+ALWAYS\s+)?AS\s*\(", re.IGNORECASE)
|
|
14
|
+
_CHECK = re.compile(r"\bCHECK\s*\(", re.IGNORECASE)
|
|
15
|
+
_CONSTRAINT_NAME = re.compile(
|
|
16
|
+
r"\bCONSTRAINT\s+(?:\"((?:\"\"|[^\"])*)\"|`((?:``|[^`])*)`|"
|
|
17
|
+
r"\[([^]]*)\]|([^\s,()]+))\s*$",
|
|
18
|
+
re.IGNORECASE,
|
|
19
|
+
)
|
|
20
|
+
_ORDERING = re.compile(r"\s+(ASC|DESC)\s*$", re.IGNORECASE)
|
|
21
|
+
_COLLATION = re.compile(r"\s+COLLATE\s+\S+\s*$", re.IGNORECASE)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class ParsedIndexTerm:
|
|
26
|
+
expression: str
|
|
27
|
+
descending: bool
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class ParsedCheck:
|
|
32
|
+
name: str | None
|
|
33
|
+
expression: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_constraints(create_sql: str | None) -> tuple[ParsedCheck, ...]:
|
|
37
|
+
if create_sql is None:
|
|
38
|
+
return ()
|
|
39
|
+
body, _ = _first_parenthesized(create_sql)
|
|
40
|
+
checks: list[ParsedCheck] = []
|
|
41
|
+
for definition in split_top_level(body):
|
|
42
|
+
for match in _CHECK.finditer(definition):
|
|
43
|
+
if not _is_top_level(definition, match.start()):
|
|
44
|
+
continue
|
|
45
|
+
expression, _ = _balanced_from(definition, match.end() - 1)
|
|
46
|
+
checks.append(
|
|
47
|
+
ParsedCheck(
|
|
48
|
+
_constraint_name(definition[: match.start()]), expression.strip()
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
return tuple(checks)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def generated_columns(create_sql: str | None) -> dict[str, tuple[str, bool]]:
|
|
55
|
+
if create_sql is None:
|
|
56
|
+
return {}
|
|
57
|
+
body, _ = _first_parenthesized(create_sql)
|
|
58
|
+
generated: dict[str, tuple[str, bool]] = {}
|
|
59
|
+
for definition in split_top_level(body):
|
|
60
|
+
name, remainder = _column_definition(definition)
|
|
61
|
+
if name is None:
|
|
62
|
+
continue
|
|
63
|
+
match = _GENERATED.search(remainder)
|
|
64
|
+
if match is None or not _is_top_level(remainder, match.start()):
|
|
65
|
+
continue
|
|
66
|
+
expression, closing = _balanced_from(remainder, match.end() - 1)
|
|
67
|
+
storage = remainder[closing + 1 :].upper()
|
|
68
|
+
generated[name] = (expression.strip(), "STORED" in storage)
|
|
69
|
+
return generated
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def view_query(create_sql: str | None) -> str | None:
|
|
73
|
+
if create_sql is None:
|
|
74
|
+
return None
|
|
75
|
+
position = _top_level_keyword(create_sql, "AS")
|
|
76
|
+
if position is None:
|
|
77
|
+
return None
|
|
78
|
+
query = create_sql[position + 2 :].strip()
|
|
79
|
+
return query or None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def index_parts(create_sql: str) -> tuple[tuple[ParsedIndexTerm, ...], str | None]:
|
|
83
|
+
body, closing = _first_parenthesized(create_sql)
|
|
84
|
+
terms = tuple(_index_term(term) for term in split_top_level(body))
|
|
85
|
+
suffix = create_sql[closing + 1 :]
|
|
86
|
+
where_position = _top_level_keyword(suffix, "WHERE")
|
|
87
|
+
predicate = None
|
|
88
|
+
if where_position is not None:
|
|
89
|
+
candidate = suffix[where_position + 5 :].strip()
|
|
90
|
+
predicate = candidate or None
|
|
91
|
+
return terms, predicate
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def table_options(create_sql: str | None) -> tuple[bool, bool]:
|
|
95
|
+
if create_sql is None:
|
|
96
|
+
return False, False
|
|
97
|
+
_, closing = _first_parenthesized(create_sql)
|
|
98
|
+
suffix = create_sql[closing + 1 :]
|
|
99
|
+
without_rowid = re.search(r"\bWITHOUT\s+ROWID\b", suffix, re.IGNORECASE)
|
|
100
|
+
strict = re.search(r"\bSTRICT\b", suffix, re.IGNORECASE)
|
|
101
|
+
return without_rowid is not None, strict is not None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def descending_primary_columns(create_sql: str | None) -> frozenset[str]:
|
|
105
|
+
if create_sql is None:
|
|
106
|
+
return frozenset()
|
|
107
|
+
body, _ = _first_parenthesized(create_sql)
|
|
108
|
+
names: set[str] = set()
|
|
109
|
+
for definition in split_top_level(body):
|
|
110
|
+
name, remainder = _column_definition(definition)
|
|
111
|
+
if name is not None and re.search(
|
|
112
|
+
r"\bPRIMARY\s+KEY\s+DESC\b", remainder, re.IGNORECASE
|
|
113
|
+
):
|
|
114
|
+
names.add(name)
|
|
115
|
+
return frozenset(names)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def split_top_level(value: str) -> tuple[str, ...]:
|
|
119
|
+
parts: list[str] = []
|
|
120
|
+
start = 0
|
|
121
|
+
depth = 0
|
|
122
|
+
quote: str | None = None
|
|
123
|
+
index = 0
|
|
124
|
+
while index < len(value):
|
|
125
|
+
character = value[index]
|
|
126
|
+
if quote is not None:
|
|
127
|
+
index, quote = _advance_quote(value, index, quote)
|
|
128
|
+
continue
|
|
129
|
+
if character in {'"', "'", "`", "["}:
|
|
130
|
+
quote = "]" if character == "[" else character
|
|
131
|
+
elif character == "(":
|
|
132
|
+
depth += 1
|
|
133
|
+
elif character == ")":
|
|
134
|
+
depth -= 1
|
|
135
|
+
elif character == "," and depth == 0:
|
|
136
|
+
parts.append(value[start:index].strip())
|
|
137
|
+
start = index + 1
|
|
138
|
+
index += 1
|
|
139
|
+
parts.append(value[start:].strip())
|
|
140
|
+
return tuple(part for part in parts if part)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _column_definition(definition: str) -> tuple[str | None, str]:
|
|
144
|
+
match = _IDENTIFIER.match(definition)
|
|
145
|
+
if match is None:
|
|
146
|
+
return None, definition
|
|
147
|
+
raw_name = next(group for group in match.groups() if group is not None)
|
|
148
|
+
name = raw_name.replace('""', '"').replace("``", "`")
|
|
149
|
+
if name.upper() in {"CHECK", "CONSTRAINT", "FOREIGN", "PRIMARY", "UNIQUE"}:
|
|
150
|
+
return None, definition
|
|
151
|
+
return name, definition[match.end() :]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _index_term(value: str) -> ParsedIndexTerm:
|
|
155
|
+
order = _ORDERING.search(value)
|
|
156
|
+
descending = order is not None and order.group(1).upper() == "DESC"
|
|
157
|
+
expression = value if order is None else value[: order.start()]
|
|
158
|
+
expression = _COLLATION.sub("", expression).strip()
|
|
159
|
+
return ParsedIndexTerm(expression, descending)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _constraint_name(prefix: str) -> str | None:
|
|
163
|
+
match = _CONSTRAINT_NAME.search(prefix)
|
|
164
|
+
if match is None:
|
|
165
|
+
return None
|
|
166
|
+
raw_name = next(group for group in match.groups() if group is not None)
|
|
167
|
+
return raw_name.replace('""', '"').replace("``", "`")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _first_parenthesized(value: str) -> tuple[str, int]:
|
|
171
|
+
quote: str | None = None
|
|
172
|
+
index = 0
|
|
173
|
+
while index < len(value):
|
|
174
|
+
character = value[index]
|
|
175
|
+
if quote is not None:
|
|
176
|
+
index, quote = _advance_quote(value, index, quote)
|
|
177
|
+
continue
|
|
178
|
+
if character in {'"', "'", "`", "["}:
|
|
179
|
+
quote = "]" if character == "[" else character
|
|
180
|
+
elif character == "(":
|
|
181
|
+
return _balanced_from(value, index)
|
|
182
|
+
index += 1
|
|
183
|
+
message = "SQLite DDL is missing a parenthesized definition"
|
|
184
|
+
raise SchemaInspectionError(message)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _balanced_from(value: str, opening: int) -> tuple[str, int]:
|
|
188
|
+
depth = 0
|
|
189
|
+
quote: str | None = None
|
|
190
|
+
index = opening
|
|
191
|
+
while index < len(value):
|
|
192
|
+
character = value[index]
|
|
193
|
+
if quote is not None:
|
|
194
|
+
index, quote = _advance_quote(value, index, quote)
|
|
195
|
+
continue
|
|
196
|
+
if character in {'"', "'", "`", "["}:
|
|
197
|
+
quote = "]" if character == "[" else character
|
|
198
|
+
elif character == "(":
|
|
199
|
+
depth += 1
|
|
200
|
+
elif character == ")":
|
|
201
|
+
depth -= 1
|
|
202
|
+
if depth == 0:
|
|
203
|
+
return value[opening + 1 : index], index
|
|
204
|
+
index += 1
|
|
205
|
+
message = "SQLite DDL contains an unclosed parenthesis"
|
|
206
|
+
raise SchemaInspectionError(message)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _advance_quote(value: str, index: int, quote: str) -> tuple[int, str | None]:
|
|
210
|
+
if value[index] != quote:
|
|
211
|
+
return index + 1, quote
|
|
212
|
+
if quote != "]" and index + 1 < len(value) and value[index + 1] == quote:
|
|
213
|
+
return index + 2, quote
|
|
214
|
+
return index + 1, None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _top_level_keyword(value: str, keyword: str) -> int | None:
|
|
218
|
+
pattern = re.compile(rf"\b{keyword}\b", re.IGNORECASE)
|
|
219
|
+
for match in pattern.finditer(value):
|
|
220
|
+
if _is_top_level(value, match.start()):
|
|
221
|
+
return match.start()
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _is_top_level(value: str, stop: int) -> bool:
|
|
226
|
+
depth = 0
|
|
227
|
+
quote: str | None = None
|
|
228
|
+
index = 0
|
|
229
|
+
while index < stop:
|
|
230
|
+
character = value[index]
|
|
231
|
+
if quote is not None:
|
|
232
|
+
index, quote = _advance_quote(value, index, quote)
|
|
233
|
+
continue
|
|
234
|
+
if character in {'"', "'", "`", "["}:
|
|
235
|
+
quote = "]" if character == "[" else character
|
|
236
|
+
elif character == "(":
|
|
237
|
+
depth += 1
|
|
238
|
+
elif character == ")":
|
|
239
|
+
depth -= 1
|
|
240
|
+
index += 1
|
|
241
|
+
return depth == 0 and quote is None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
__all__ = (
|
|
245
|
+
"ParsedCheck",
|
|
246
|
+
"ParsedIndexTerm",
|
|
247
|
+
"check_constraints",
|
|
248
|
+
"descending_primary_columns",
|
|
249
|
+
"generated_columns",
|
|
250
|
+
"index_parts",
|
|
251
|
+
"split_top_level",
|
|
252
|
+
"table_options",
|
|
253
|
+
"view_query",
|
|
254
|
+
)
|