d365fo-client 0.1.0__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.
- d365fo_client/__init__.py +305 -0
- d365fo_client/auth.py +93 -0
- d365fo_client/cli.py +700 -0
- d365fo_client/client.py +1454 -0
- d365fo_client/config.py +304 -0
- d365fo_client/crud.py +200 -0
- d365fo_client/exceptions.py +49 -0
- d365fo_client/labels.py +528 -0
- d365fo_client/main.py +502 -0
- d365fo_client/mcp/__init__.py +16 -0
- d365fo_client/mcp/client_manager.py +276 -0
- d365fo_client/mcp/main.py +98 -0
- d365fo_client/mcp/models.py +371 -0
- d365fo_client/mcp/prompts/__init__.py +43 -0
- d365fo_client/mcp/prompts/action_execution.py +480 -0
- d365fo_client/mcp/prompts/sequence_analysis.py +349 -0
- d365fo_client/mcp/resources/__init__.py +15 -0
- d365fo_client/mcp/resources/database_handler.py +555 -0
- d365fo_client/mcp/resources/entity_handler.py +176 -0
- d365fo_client/mcp/resources/environment_handler.py +132 -0
- d365fo_client/mcp/resources/metadata_handler.py +283 -0
- d365fo_client/mcp/resources/query_handler.py +135 -0
- d365fo_client/mcp/server.py +432 -0
- d365fo_client/mcp/tools/__init__.py +17 -0
- d365fo_client/mcp/tools/connection_tools.py +175 -0
- d365fo_client/mcp/tools/crud_tools.py +579 -0
- d365fo_client/mcp/tools/database_tools.py +813 -0
- d365fo_client/mcp/tools/label_tools.py +189 -0
- d365fo_client/mcp/tools/metadata_tools.py +766 -0
- d365fo_client/mcp/tools/profile_tools.py +706 -0
- d365fo_client/metadata_api.py +793 -0
- d365fo_client/metadata_v2/__init__.py +59 -0
- d365fo_client/metadata_v2/cache_v2.py +1372 -0
- d365fo_client/metadata_v2/database_v2.py +585 -0
- d365fo_client/metadata_v2/global_version_manager.py +573 -0
- d365fo_client/metadata_v2/search_engine_v2.py +423 -0
- d365fo_client/metadata_v2/sync_manager_v2.py +819 -0
- d365fo_client/metadata_v2/version_detector.py +439 -0
- d365fo_client/models.py +862 -0
- d365fo_client/output.py +181 -0
- d365fo_client/profile_manager.py +342 -0
- d365fo_client/profiles.py +178 -0
- d365fo_client/query.py +162 -0
- d365fo_client/session.py +60 -0
- d365fo_client/utils.py +196 -0
- d365fo_client-0.1.0.dist-info/METADATA +1084 -0
- d365fo_client-0.1.0.dist-info/RECORD +51 -0
- d365fo_client-0.1.0.dist-info/WHEEL +5 -0
- d365fo_client-0.1.0.dist-info/entry_points.txt +3 -0
- d365fo_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- d365fo_client-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,585 @@
|
|
1
|
+
"""Enhanced database schema with global version management."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any, Dict, Optional
|
6
|
+
|
7
|
+
import aiosqlite
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
class DatabaseSchemaV2:
|
13
|
+
"""Database schema manager for metadata v2"""
|
14
|
+
|
15
|
+
@staticmethod
|
16
|
+
async def create_schema(db: aiosqlite.Connection):
|
17
|
+
"""Create complete database schema for metadata v2"""
|
18
|
+
|
19
|
+
# Core environment tracking (enhanced)
|
20
|
+
await db.execute(
|
21
|
+
"""
|
22
|
+
CREATE TABLE IF NOT EXISTS metadata_environments (
|
23
|
+
id INTEGER PRIMARY KEY,
|
24
|
+
base_url TEXT NOT NULL UNIQUE,
|
25
|
+
environment_name TEXT,
|
26
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
27
|
+
last_sync_at TIMESTAMP,
|
28
|
+
last_version_check TIMESTAMP,
|
29
|
+
is_active BOOLEAN DEFAULT 1
|
30
|
+
)
|
31
|
+
"""
|
32
|
+
)
|
33
|
+
|
34
|
+
# Global version registry (NEW)
|
35
|
+
await db.execute(
|
36
|
+
"""
|
37
|
+
CREATE TABLE IF NOT EXISTS global_versions (
|
38
|
+
id INTEGER PRIMARY KEY,
|
39
|
+
version_hash TEXT UNIQUE NOT NULL,
|
40
|
+
modules_hash TEXT UNIQUE NOT NULL,
|
41
|
+
first_seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
42
|
+
last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
43
|
+
reference_count INTEGER DEFAULT 0,
|
44
|
+
metadata_size_bytes INTEGER DEFAULT 0,
|
45
|
+
created_by_environment_id INTEGER REFERENCES metadata_environments(id)
|
46
|
+
)
|
47
|
+
"""
|
48
|
+
)
|
49
|
+
|
50
|
+
# Environment to global version mapping (NEW)
|
51
|
+
await db.execute(
|
52
|
+
"""
|
53
|
+
CREATE TABLE IF NOT EXISTS environment_versions (
|
54
|
+
environment_id INTEGER REFERENCES metadata_environments(id),
|
55
|
+
global_version_id INTEGER REFERENCES global_versions(id),
|
56
|
+
detected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
57
|
+
is_active BOOLEAN DEFAULT 1,
|
58
|
+
sync_status TEXT DEFAULT 'pending', -- pending|syncing|completed|failed
|
59
|
+
last_sync_duration_ms INTEGER,
|
60
|
+
PRIMARY KEY (environment_id, global_version_id)
|
61
|
+
)
|
62
|
+
"""
|
63
|
+
)
|
64
|
+
|
65
|
+
# Sample modules for global versions (NEW)
|
66
|
+
await db.execute(
|
67
|
+
"""
|
68
|
+
CREATE TABLE IF NOT EXISTS global_version_modules (
|
69
|
+
id INTEGER PRIMARY KEY,
|
70
|
+
global_version_id INTEGER REFERENCES global_versions(id),
|
71
|
+
module_id TEXT NOT NULL,
|
72
|
+
module_name TEXT,
|
73
|
+
version TEXT,
|
74
|
+
publisher TEXT,
|
75
|
+
display_name TEXT,
|
76
|
+
sort_order INTEGER DEFAULT 0
|
77
|
+
)
|
78
|
+
"""
|
79
|
+
)
|
80
|
+
|
81
|
+
# Enhanced metadata versioning
|
82
|
+
await db.execute(
|
83
|
+
"""
|
84
|
+
CREATE TABLE IF NOT EXISTS metadata_versions (
|
85
|
+
id INTEGER PRIMARY KEY,
|
86
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
87
|
+
application_version TEXT,
|
88
|
+
platform_version TEXT,
|
89
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
90
|
+
sync_completed_at TIMESTAMP,
|
91
|
+
entity_count INTEGER DEFAULT 0,
|
92
|
+
action_count INTEGER DEFAULT 0,
|
93
|
+
enumeration_count INTEGER DEFAULT 0,
|
94
|
+
label_count INTEGER DEFAULT 0
|
95
|
+
)
|
96
|
+
"""
|
97
|
+
)
|
98
|
+
|
99
|
+
# Version-aware metadata tables (enhanced with global_version_id)
|
100
|
+
|
101
|
+
# Data entities
|
102
|
+
await db.execute(
|
103
|
+
"""
|
104
|
+
CREATE TABLE IF NOT EXISTS data_entities (
|
105
|
+
id INTEGER PRIMARY KEY,
|
106
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
107
|
+
name TEXT NOT NULL,
|
108
|
+
public_entity_name TEXT,
|
109
|
+
public_collection_name TEXT,
|
110
|
+
label_id TEXT,
|
111
|
+
label_text TEXT,
|
112
|
+
entity_category TEXT,
|
113
|
+
data_service_enabled BOOLEAN DEFAULT 1,
|
114
|
+
data_management_enabled BOOLEAN DEFAULT 1,
|
115
|
+
is_read_only BOOLEAN DEFAULT 0,
|
116
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
117
|
+
)
|
118
|
+
"""
|
119
|
+
)
|
120
|
+
|
121
|
+
# Public entities
|
122
|
+
await db.execute(
|
123
|
+
"""
|
124
|
+
CREATE TABLE IF NOT EXISTS public_entities (
|
125
|
+
id INTEGER PRIMARY KEY,
|
126
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
127
|
+
name TEXT NOT NULL,
|
128
|
+
entity_set_name TEXT,
|
129
|
+
label_id TEXT,
|
130
|
+
label_text TEXT,
|
131
|
+
is_read_only BOOLEAN DEFAULT 0,
|
132
|
+
configuration_enabled BOOLEAN DEFAULT 1,
|
133
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
134
|
+
)
|
135
|
+
"""
|
136
|
+
)
|
137
|
+
|
138
|
+
# Entity properties (version-aware)
|
139
|
+
await db.execute(
|
140
|
+
"""
|
141
|
+
CREATE TABLE IF NOT EXISTS entity_properties (
|
142
|
+
id INTEGER PRIMARY KEY,
|
143
|
+
entity_id INTEGER NOT NULL REFERENCES public_entities(id),
|
144
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
145
|
+
name TEXT NOT NULL,
|
146
|
+
type_name TEXT,
|
147
|
+
data_type TEXT,
|
148
|
+
odata_xpp_type TEXT,
|
149
|
+
label_id TEXT,
|
150
|
+
label_text TEXT,
|
151
|
+
is_key BOOLEAN DEFAULT 0,
|
152
|
+
is_mandatory BOOLEAN DEFAULT 0,
|
153
|
+
configuration_enabled BOOLEAN DEFAULT 1,
|
154
|
+
allow_edit BOOLEAN DEFAULT 1,
|
155
|
+
allow_edit_on_create BOOLEAN DEFAULT 1,
|
156
|
+
is_dimension BOOLEAN DEFAULT 0,
|
157
|
+
dimension_relation TEXT,
|
158
|
+
is_dynamic_dimension BOOLEAN DEFAULT 0,
|
159
|
+
dimension_legal_entity_property TEXT,
|
160
|
+
dimension_type_property TEXT,
|
161
|
+
property_order INTEGER DEFAULT 0
|
162
|
+
)
|
163
|
+
"""
|
164
|
+
)
|
165
|
+
|
166
|
+
# Navigation properties (version-aware)
|
167
|
+
await db.execute(
|
168
|
+
"""
|
169
|
+
CREATE TABLE IF NOT EXISTS navigation_properties (
|
170
|
+
id INTEGER PRIMARY KEY,
|
171
|
+
entity_id INTEGER NOT NULL REFERENCES public_entities(id),
|
172
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
173
|
+
name TEXT NOT NULL,
|
174
|
+
related_entity TEXT,
|
175
|
+
related_relation_name TEXT,
|
176
|
+
cardinality TEXT DEFAULT 'Single'
|
177
|
+
)
|
178
|
+
"""
|
179
|
+
)
|
180
|
+
|
181
|
+
# Relation constraints (version-aware)
|
182
|
+
await db.execute(
|
183
|
+
"""
|
184
|
+
CREATE TABLE IF NOT EXISTS relation_constraints (
|
185
|
+
id INTEGER PRIMARY KEY,
|
186
|
+
navigation_property_id INTEGER NOT NULL REFERENCES navigation_properties(id),
|
187
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
188
|
+
constraint_type TEXT NOT NULL,
|
189
|
+
property_name TEXT,
|
190
|
+
referenced_property TEXT,
|
191
|
+
related_property TEXT,
|
192
|
+
fixed_value INTEGER,
|
193
|
+
fixed_value_str TEXT
|
194
|
+
)
|
195
|
+
"""
|
196
|
+
)
|
197
|
+
|
198
|
+
# Property groups (version-aware)
|
199
|
+
await db.execute(
|
200
|
+
"""
|
201
|
+
CREATE TABLE IF NOT EXISTS property_groups (
|
202
|
+
id INTEGER PRIMARY KEY,
|
203
|
+
entity_id INTEGER NOT NULL REFERENCES public_entities(id),
|
204
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
205
|
+
name TEXT NOT NULL
|
206
|
+
)
|
207
|
+
"""
|
208
|
+
)
|
209
|
+
|
210
|
+
# Property group members (version-aware)
|
211
|
+
await db.execute(
|
212
|
+
"""
|
213
|
+
CREATE TABLE IF NOT EXISTS property_group_members (
|
214
|
+
id INTEGER PRIMARY KEY,
|
215
|
+
property_group_id INTEGER NOT NULL REFERENCES property_groups(id),
|
216
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
217
|
+
property_name TEXT NOT NULL,
|
218
|
+
member_order INTEGER DEFAULT 0
|
219
|
+
)
|
220
|
+
"""
|
221
|
+
)
|
222
|
+
|
223
|
+
# Entity actions (version-aware)
|
224
|
+
await db.execute(
|
225
|
+
"""
|
226
|
+
CREATE TABLE IF NOT EXISTS entity_actions (
|
227
|
+
id INTEGER PRIMARY KEY,
|
228
|
+
entity_id INTEGER NOT NULL REFERENCES public_entities(id),
|
229
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
230
|
+
name TEXT NOT NULL,
|
231
|
+
binding_kind TEXT DEFAULT 'BoundToEntitySet',
|
232
|
+
entity_name TEXT,
|
233
|
+
entity_set_name TEXT,
|
234
|
+
return_type_name TEXT,
|
235
|
+
return_is_collection BOOLEAN DEFAULT 0,
|
236
|
+
return_odata_xpp_type TEXT,
|
237
|
+
field_lookup TEXT
|
238
|
+
)
|
239
|
+
"""
|
240
|
+
)
|
241
|
+
|
242
|
+
# Action parameters (version-aware)
|
243
|
+
await db.execute(
|
244
|
+
"""
|
245
|
+
CREATE TABLE IF NOT EXISTS action_parameters (
|
246
|
+
id INTEGER PRIMARY KEY,
|
247
|
+
action_id INTEGER NOT NULL REFERENCES entity_actions(id),
|
248
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
249
|
+
name TEXT NOT NULL,
|
250
|
+
type_name TEXT,
|
251
|
+
is_collection BOOLEAN DEFAULT 0,
|
252
|
+
odata_xpp_type TEXT,
|
253
|
+
parameter_order INTEGER DEFAULT 0
|
254
|
+
)
|
255
|
+
"""
|
256
|
+
)
|
257
|
+
|
258
|
+
# Enumerations (version-aware)
|
259
|
+
await db.execute(
|
260
|
+
"""
|
261
|
+
CREATE TABLE IF NOT EXISTS enumerations (
|
262
|
+
id INTEGER PRIMARY KEY,
|
263
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
264
|
+
name TEXT NOT NULL,
|
265
|
+
label_id TEXT,
|
266
|
+
label_text TEXT,
|
267
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
268
|
+
)
|
269
|
+
"""
|
270
|
+
)
|
271
|
+
|
272
|
+
# Enumeration members (version-aware)
|
273
|
+
await db.execute(
|
274
|
+
"""
|
275
|
+
CREATE TABLE IF NOT EXISTS enumeration_members (
|
276
|
+
id INTEGER PRIMARY KEY,
|
277
|
+
enumeration_id INTEGER NOT NULL REFERENCES enumerations(id),
|
278
|
+
global_version_id INTEGER NOT NULL REFERENCES global_versions(id),
|
279
|
+
name TEXT NOT NULL,
|
280
|
+
value INTEGER NOT NULL,
|
281
|
+
label_id TEXT,
|
282
|
+
label_text TEXT,
|
283
|
+
configuration_enabled BOOLEAN DEFAULT 1,
|
284
|
+
member_order INTEGER DEFAULT 0
|
285
|
+
)
|
286
|
+
"""
|
287
|
+
)
|
288
|
+
|
289
|
+
# Labels cache (version-aware)
|
290
|
+
await db.execute(
|
291
|
+
"""
|
292
|
+
CREATE TABLE IF NOT EXISTS labels_cache (
|
293
|
+
id INTEGER PRIMARY KEY,
|
294
|
+
global_version_id INTEGER REFERENCES global_versions(id),
|
295
|
+
label_id TEXT NOT NULL,
|
296
|
+
language TEXT NOT NULL DEFAULT 'en-US',
|
297
|
+
label_text TEXT,
|
298
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
299
|
+
expires_at TIMESTAMP,
|
300
|
+
hit_count INTEGER DEFAULT 0,
|
301
|
+
last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
302
|
+
UNIQUE(global_version_id, label_id, language)
|
303
|
+
)
|
304
|
+
"""
|
305
|
+
)
|
306
|
+
|
307
|
+
# FTS5 search index (version-aware)
|
308
|
+
await db.execute(
|
309
|
+
"""
|
310
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS metadata_search_v2 USING fts5(
|
311
|
+
name,
|
312
|
+
entity_type,
|
313
|
+
description,
|
314
|
+
properties,
|
315
|
+
labels,
|
316
|
+
global_version_id UNINDEXED,
|
317
|
+
entity_id UNINDEXED,
|
318
|
+
content='',
|
319
|
+
contentless_delete=1
|
320
|
+
)
|
321
|
+
"""
|
322
|
+
)
|
323
|
+
|
324
|
+
await db.commit()
|
325
|
+
logger.info("Database schema v2 created successfully")
|
326
|
+
|
327
|
+
@staticmethod
|
328
|
+
async def create_indexes(db: aiosqlite.Connection):
|
329
|
+
"""Create optimized indexes for version-aware queries"""
|
330
|
+
|
331
|
+
indexes = [
|
332
|
+
# Global version indexes
|
333
|
+
"CREATE INDEX IF NOT EXISTS idx_global_versions_hash ON global_versions(version_hash)",
|
334
|
+
"CREATE INDEX IF NOT EXISTS idx_global_versions_modules_hash ON global_versions(modules_hash)",
|
335
|
+
"CREATE INDEX IF NOT EXISTS idx_global_versions_last_used ON global_versions(last_used_at)",
|
336
|
+
# Environment version indexes
|
337
|
+
"CREATE INDEX IF NOT EXISTS idx_env_versions_active ON environment_versions(environment_id, is_active)",
|
338
|
+
"CREATE INDEX IF NOT EXISTS idx_env_versions_global ON environment_versions(global_version_id, is_active)",
|
339
|
+
# Version-aware entity indexes
|
340
|
+
"CREATE INDEX IF NOT EXISTS idx_data_entities_version ON data_entities(global_version_id, name)",
|
341
|
+
"CREATE INDEX IF NOT EXISTS idx_public_entities_version ON public_entities(global_version_id, name)",
|
342
|
+
"CREATE INDEX IF NOT EXISTS idx_entity_properties_version ON entity_properties(global_version_id, entity_id)",
|
343
|
+
"CREATE INDEX IF NOT EXISTS idx_navigation_props_version ON navigation_properties(global_version_id, entity_id)",
|
344
|
+
"CREATE INDEX IF NOT EXISTS idx_entity_actions_version ON entity_actions(global_version_id, entity_id)",
|
345
|
+
"CREATE INDEX IF NOT EXISTS idx_enumerations_version ON enumerations(global_version_id, name)",
|
346
|
+
# Labels indexes
|
347
|
+
"CREATE INDEX IF NOT EXISTS idx_labels_version_lookup ON labels_cache(global_version_id, label_id, language)",
|
348
|
+
"CREATE INDEX IF NOT EXISTS idx_labels_expires ON labels_cache(expires_at)",
|
349
|
+
# Search performance indexes
|
350
|
+
"CREATE INDEX IF NOT EXISTS idx_data_entities_search ON data_entities(global_version_id, data_service_enabled, entity_category)",
|
351
|
+
"CREATE INDEX IF NOT EXISTS idx_public_entities_search ON public_entities(global_version_id, is_read_only)",
|
352
|
+
# Global version modules index
|
353
|
+
"CREATE INDEX IF NOT EXISTS idx_global_version_modules ON global_version_modules(global_version_id, module_id)",
|
354
|
+
]
|
355
|
+
|
356
|
+
for index_sql in indexes:
|
357
|
+
try:
|
358
|
+
await db.execute(index_sql)
|
359
|
+
except Exception as e:
|
360
|
+
logger.warning(f"Failed to create index: {e}")
|
361
|
+
|
362
|
+
await db.commit()
|
363
|
+
logger.info("Database indexes v2 created successfully")
|
364
|
+
|
365
|
+
|
366
|
+
class MetadataDatabaseV2:
|
367
|
+
"""Enhanced metadata database with global version support"""
|
368
|
+
|
369
|
+
def __init__(self, db_path: Path):
|
370
|
+
"""Initialize database with path
|
371
|
+
|
372
|
+
Args:
|
373
|
+
db_path: Path to SQLite database file
|
374
|
+
"""
|
375
|
+
self.db_path = db_path
|
376
|
+
self._ensure_database_directory()
|
377
|
+
|
378
|
+
def _ensure_database_directory(self):
|
379
|
+
"""Ensure database directory exists"""
|
380
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
381
|
+
|
382
|
+
async def initialize(self):
|
383
|
+
"""Initialize database with v2 schema"""
|
384
|
+
async with aiosqlite.connect(self.db_path) as db:
|
385
|
+
await DatabaseSchemaV2.create_schema(db)
|
386
|
+
await DatabaseSchemaV2.create_indexes(db)
|
387
|
+
|
388
|
+
# Enable foreign key constraints
|
389
|
+
await db.execute("PRAGMA foreign_keys = ON")
|
390
|
+
await db.execute("PRAGMA journal_mode = WAL")
|
391
|
+
await db.commit()
|
392
|
+
|
393
|
+
logger.info(f"Metadata database v2 initialized: {self.db_path}")
|
394
|
+
|
395
|
+
async def get_or_create_environment(self, base_url: str) -> int:
|
396
|
+
"""Get or create environment ID
|
397
|
+
|
398
|
+
Args:
|
399
|
+
base_url: Environment base URL
|
400
|
+
|
401
|
+
Returns:
|
402
|
+
Environment ID
|
403
|
+
"""
|
404
|
+
async with aiosqlite.connect(self.db_path) as db:
|
405
|
+
# Try to find existing environment
|
406
|
+
cursor = await db.execute(
|
407
|
+
"SELECT id FROM metadata_environments WHERE base_url = ?", (base_url,)
|
408
|
+
)
|
409
|
+
|
410
|
+
row = await cursor.fetchone()
|
411
|
+
if row:
|
412
|
+
return row[0]
|
413
|
+
|
414
|
+
# Create new environment
|
415
|
+
environment_name = self._extract_environment_name(base_url)
|
416
|
+
cursor = await db.execute(
|
417
|
+
"""INSERT INTO metadata_environments (base_url, environment_name)
|
418
|
+
VALUES (?, ?)""",
|
419
|
+
(base_url, environment_name),
|
420
|
+
)
|
421
|
+
|
422
|
+
environment_id = cursor.lastrowid
|
423
|
+
await db.commit()
|
424
|
+
|
425
|
+
logger.info(f"Created environment {environment_id}: {environment_name}")
|
426
|
+
return environment_id
|
427
|
+
|
428
|
+
def _extract_environment_name(self, base_url: str) -> str:
|
429
|
+
"""Extract environment name from URL
|
430
|
+
|
431
|
+
Args:
|
432
|
+
base_url: Full environment URL
|
433
|
+
|
434
|
+
Returns:
|
435
|
+
Extracted environment name
|
436
|
+
"""
|
437
|
+
from urllib.parse import urlparse
|
438
|
+
|
439
|
+
parsed = urlparse(base_url)
|
440
|
+
hostname = parsed.hostname or base_url
|
441
|
+
return hostname.split(".")[0] if "." in hostname else hostname
|
442
|
+
|
443
|
+
async def get_global_version_metadata_counts(
|
444
|
+
self, global_version_id: int
|
445
|
+
) -> Dict[str, int]:
|
446
|
+
"""Get metadata counts for a global version
|
447
|
+
|
448
|
+
Args:
|
449
|
+
global_version_id: Global version ID to get counts for
|
450
|
+
|
451
|
+
Returns:
|
452
|
+
Dictionary with counts for each metadata type
|
453
|
+
"""
|
454
|
+
async with aiosqlite.connect(self.db_path) as db:
|
455
|
+
counts = {}
|
456
|
+
|
457
|
+
tables = [
|
458
|
+
("data_entities", "entities"),
|
459
|
+
("public_entities", "public_entities"),
|
460
|
+
("entity_properties", "properties"),
|
461
|
+
("entity_actions", "actions"),
|
462
|
+
("enumerations", "enumerations"),
|
463
|
+
("labels_cache", "labels"),
|
464
|
+
]
|
465
|
+
|
466
|
+
for table, key in tables:
|
467
|
+
cursor = await db.execute(
|
468
|
+
f"SELECT COUNT(*) FROM {table} WHERE global_version_id = ?",
|
469
|
+
(global_version_id,),
|
470
|
+
)
|
471
|
+
counts[key] = (await cursor.fetchone())[0]
|
472
|
+
|
473
|
+
return counts
|
474
|
+
|
475
|
+
async def get_database_statistics(self) -> Dict[str, Any]:
|
476
|
+
"""Get comprehensive database statistics
|
477
|
+
|
478
|
+
Returns:
|
479
|
+
Dictionary with database statistics
|
480
|
+
"""
|
481
|
+
async with aiosqlite.connect(self.db_path) as db:
|
482
|
+
stats = {}
|
483
|
+
|
484
|
+
# Basic table counts
|
485
|
+
tables = [
|
486
|
+
"metadata_environments",
|
487
|
+
"global_versions",
|
488
|
+
"environment_versions",
|
489
|
+
"global_version_modules",
|
490
|
+
"metadata_versions",
|
491
|
+
"data_entities",
|
492
|
+
"public_entities",
|
493
|
+
"entity_properties",
|
494
|
+
"navigation_properties",
|
495
|
+
"entity_actions",
|
496
|
+
"enumerations",
|
497
|
+
"labels_cache",
|
498
|
+
]
|
499
|
+
|
500
|
+
for table in tables:
|
501
|
+
cursor = await db.execute(f"SELECT COUNT(*) FROM {table}")
|
502
|
+
stats[f"{table}_count"] = (await cursor.fetchone())[0]
|
503
|
+
|
504
|
+
# Global version statistics
|
505
|
+
cursor = await db.execute(
|
506
|
+
"""SELECT
|
507
|
+
COUNT(*) as total_versions,
|
508
|
+
SUM(reference_count) as total_references,
|
509
|
+
AVG(reference_count) as avg_references,
|
510
|
+
MAX(reference_count) as max_references
|
511
|
+
FROM global_versions"""
|
512
|
+
)
|
513
|
+
version_stats = await cursor.fetchone()
|
514
|
+
stats["version_statistics"] = {
|
515
|
+
"total_versions": version_stats[0],
|
516
|
+
"total_references": version_stats[1] or 0,
|
517
|
+
"avg_references": round(version_stats[2] or 0, 2),
|
518
|
+
"max_references": version_stats[3] or 0,
|
519
|
+
}
|
520
|
+
|
521
|
+
# Environment statistics
|
522
|
+
cursor = await db.execute(
|
523
|
+
"""SELECT
|
524
|
+
COUNT(DISTINCT me.id) as total_environments,
|
525
|
+
COUNT(DISTINCT ev.global_version_id) as linked_versions
|
526
|
+
FROM metadata_environments me
|
527
|
+
LEFT JOIN environment_versions ev ON me.id = ev.environment_id
|
528
|
+
WHERE ev.is_active = 1"""
|
529
|
+
)
|
530
|
+
env_stats = await cursor.fetchone()
|
531
|
+
stats["environment_statistics"] = {
|
532
|
+
"total_environments": env_stats[0],
|
533
|
+
"linked_versions": env_stats[1] or 0,
|
534
|
+
}
|
535
|
+
|
536
|
+
# Database file size
|
537
|
+
try:
|
538
|
+
db_size = self.db_path.stat().st_size
|
539
|
+
stats["database_size_bytes"] = db_size
|
540
|
+
stats["database_size_mb"] = round(db_size / (1024 * 1024), 2)
|
541
|
+
except Exception:
|
542
|
+
stats["database_size_bytes"] = None
|
543
|
+
stats["database_size_mb"] = None
|
544
|
+
|
545
|
+
return stats
|
546
|
+
|
547
|
+
async def vacuum_database(self) -> bool:
|
548
|
+
"""Vacuum database to reclaim space
|
549
|
+
|
550
|
+
Returns:
|
551
|
+
True if successful, False otherwise
|
552
|
+
"""
|
553
|
+
try:
|
554
|
+
async with aiosqlite.connect(self.db_path) as db:
|
555
|
+
await db.execute("VACUUM")
|
556
|
+
await db.commit()
|
557
|
+
logger.info("Database vacuum completed successfully")
|
558
|
+
return True
|
559
|
+
except Exception as e:
|
560
|
+
logger.error(f"Database vacuum failed: {e}")
|
561
|
+
return False
|
562
|
+
|
563
|
+
async def check_database_integrity(self) -> Dict[str, Any]:
|
564
|
+
"""Check database integrity
|
565
|
+
|
566
|
+
Returns:
|
567
|
+
Dictionary with integrity check results
|
568
|
+
"""
|
569
|
+
async with aiosqlite.connect(self.db_path) as db:
|
570
|
+
# Run integrity check
|
571
|
+
cursor = await db.execute("PRAGMA integrity_check")
|
572
|
+
integrity_result = await cursor.fetchone()
|
573
|
+
|
574
|
+
# Run foreign key check
|
575
|
+
cursor = await db.execute("PRAGMA foreign_key_check")
|
576
|
+
foreign_key_issues = await cursor.fetchall()
|
577
|
+
|
578
|
+
return {
|
579
|
+
"integrity_ok": integrity_result[0] == "ok",
|
580
|
+
"integrity_message": integrity_result[0],
|
581
|
+
"foreign_key_issues": len(foreign_key_issues),
|
582
|
+
"foreign_key_details": foreign_key_issues[
|
583
|
+
:10
|
584
|
+
], # Limit to first 10 issues
|
585
|
+
}
|