Source code for sqlspec.adapters.sqlite.data_dictionary

"""SQLite-specific data dictionary for metadata queries."""

from typing import TYPE_CHECKING, Any, ClassVar, cast

from mypy_extensions import mypyc_attr

from sqlspec.adapters.sqlite.core import format_identifier
from sqlspec.data_dictionary import (
    ColumnMetadata,
    ForeignKeyMetadata,
    IndexMetadata,
    TableMetadata,
    VersionInfo,
    get_data_dictionary_loader,
    get_dialect_config,
)
from sqlspec.data_dictionary.dialects.sqlite import list_sqlite_available_features, resolve_sqlite_json_type
from sqlspec.driver import SyncDataDictionaryBase

if TYPE_CHECKING:
    from sqlspec.adapters.sqlite.driver import SqliteDriver

__all__ = ("SqliteDataDictionary",)

SQLITE_INDEX_EXPRESSION_COLUMN_ID = -2


[docs] @mypyc_attr(allow_interpreted_subclasses=True, native_class=False) class SqliteDataDictionary(SyncDataDictionaryBase): """SQLite-specific sync data dictionary.""" dialect: ClassVar[str] = "sqlite"
[docs] def __init__(self) -> None: super().__init__()
[docs] def resolve_schema(self, schema: "str | None") -> "str | None": """Return a schema name using dialect defaults when missing.""" if schema is not None: return schema return get_dialect_config(type(self).dialect).default_schema
[docs] def get_version(self, driver: "SqliteDriver") -> "VersionInfo | None": """Get SQLite database version information. Args: driver: Sync database driver instance. Returns: SQLite version information or None if detection fails. """ driver_id = id(driver) # Inline cache check to avoid cross-module method call that causes mypyc segfault if driver_id in self._version_fetch_attempted: return self._version_cache.get(driver_id) # Not cached, fetch from database version_value = driver.select_value_or_none(self.get_query("version", "current")) if not version_value: self._log_version_unavailable(type(self).dialect, "missing") self.cache_version(driver_id, None) return None config = get_dialect_config(type(self).dialect) version_info = self.parse_version_with_pattern(config.version_pattern, str(version_value)) if version_info is None: self._log_version_unavailable(type(self).dialect, "parse_failed") self.cache_version(driver_id, None) return None self._log_version_detected(type(self).dialect, version_info) self.cache_version(driver_id, version_info) return version_info
[docs] def get_feature_flag(self, driver: "SqliteDriver", feature: str) -> bool: """Check if SQLite database supports a specific feature. Args: driver: Sync database driver instance. feature: Feature name to check. Returns: True if feature is supported, False otherwise. """ version_info = self.get_version(driver) return self.resolve_feature_flag(feature, version_info)
[docs] def get_optimal_type(self, driver: "SqliteDriver", type_category: str) -> str: """Get optimal SQLite type for a category. Args: driver: Sync database driver instance. type_category: Type category. Returns: SQLite-specific type name. """ config = get_dialect_config(type(self).dialect) version_info = self.get_version(driver) if type_category == "json": return resolve_sqlite_json_type(version_info) return config.get_optimal_type(type_category)
[docs] def list_available_features(self) -> "list[str]": """List available feature flags for this dialect.""" return list_sqlite_available_features()
[docs] def get_tables(self, driver: "SqliteDriver", schema: "str | None" = None) -> "list[TableMetadata]": """Get tables sorted by topological dependency order using SQLite catalog.""" schema_name = self.resolve_schema(schema) self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="tables") query_text = self._get_domain_query_text("tables", "by_schema") return driver.select(query_text, schema_name=schema_name, schema_type=TableMetadata)
[docs] def get_columns( self, driver: "SqliteDriver", table: "str | None" = None, schema: "str | None" = None ) -> "list[ColumnMetadata]": """Get column information for a table or schema.""" schema_name = self.resolve_schema(schema) schema_prefix = f"{format_identifier(schema_name)}." if schema_name else "" if table is None: self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="columns") query_text = self._get_domain_query_text("columns", "by_schema").format(schema_prefix=schema_prefix) return driver.select(query_text, schema_name=schema_name, schema_type=ColumnMetadata) self._log_table_describe(driver, schema_name=schema_name, table_name=table, operation="columns") query_text = self._get_domain_query_text("columns", "by_table").format(schema_prefix=schema_prefix) return driver.select(query_text, table_name=table, schema_name=schema_name, schema_type=ColumnMetadata)
[docs] def get_indexes( self, driver: "SqliteDriver", table: "str | None" = None, schema: "str | None" = None ) -> "list[IndexMetadata]": """Get index metadata for a table or schema.""" schema_name = self.resolve_schema(schema) if table is None: self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="indexes") schema_prefix = f"{format_identifier(schema_name)}." if schema_name else "" query_text = self._get_domain_query_text("indexes", "by_schema").format(schema_prefix=schema_prefix) rows = driver.select(query_text, schema_name=schema_name) return _sqlite_index_rows_to_metadata(rows, schema_name) self._log_table_describe(driver, schema_name=schema_name, table_name=table, operation="indexes") schema_prefix = f"{format_identifier(schema_name)}." if schema_name else "" query_text = self._get_domain_query_text("indexes", "by_table").format(schema_prefix=schema_prefix) rows = driver.select(query_text, table_name=table, schema_name=schema_name) return _sqlite_index_rows_to_metadata(rows, schema_name)
[docs] def get_foreign_keys( self, driver: "SqliteDriver", table: "str | None" = None, schema: "str | None" = None ) -> "list[ForeignKeyMetadata]": """Get foreign key metadata.""" schema_name = self.resolve_schema(schema) if table is None: self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="foreign_keys") query_text = self._get_domain_query_text("foreign_keys", "by_schema") return driver.select(query_text, schema_name=schema_name, schema_type=ForeignKeyMetadata) self._log_table_describe(driver, schema_name=schema_name, table_name=table, operation="foreign_keys") query_text = self._get_domain_query_text("foreign_keys", "by_table") return driver.select(query_text, table_name=table, schema_name=schema_name, schema_type=ForeignKeyMetadata)
def _get_domain_query_text(self, domain: str, query_name: str) -> str: """Return a required direct-domain query for SQLite.""" query_text = get_data_dictionary_loader().get_domain_query_text(type(self).dialect, domain, query_name) if query_text is None: msg = f"Missing SQLite data-dictionary query: {domain}/{query_name}" raise RuntimeError(msg) return query_text
def _sqlite_index_rows_to_metadata(rows: "list[dict[str, Any]]", schema_name: "str | None") -> "list[IndexMetadata]": """Group SQLite index_xinfo rows into the public index metadata shape.""" if not rows: return [] first_row = rows[0] if "columns" in first_row: return cast("list[IndexMetadata]", rows) grouped: dict[tuple[str | None, str, str], dict[str, Any]] = {} for row in rows: table_name_value = row.get("table_name") index_name_value = row.get("index_name") if table_name_value is None or index_name_value is None: continue row_schema = cast("str | None", row.get("schema_name", schema_name)) table_name = str(table_name_value) index_name = str(index_name_value) key = (row_schema, table_name, index_name) index_metadata = grouped.get(key) if index_metadata is None: index_metadata = { "index_name": index_name, "table_name": table_name, "columns": [], "is_primary": bool(row.get("is_primary")), } if row_schema is not None: index_metadata["schema_name"] = row_schema unique_value = row.get("is_unique") if unique_value is not None: index_metadata["is_unique"] = unique_value if row.get("is_partial") is not None: index_metadata["is_partial"] = row.get("is_partial") if row.get("index_sql") is not None: index_metadata["native_sql"] = row.get("index_sql") grouped[key] = index_metadata if row.get("is_key_column") in (0, False): continue columns = cast("list[str]", index_metadata["columns"]) column_name = row.get("column_name") if column_name is not None: columns.append(str(column_name)) elif row.get("column_id") == SQLITE_INDEX_EXPRESSION_COLUMN_ID: columns.append("<expression>") return [cast("IndexMetadata", metadata) for metadata in grouped.values()]