8from typing
import DefaultDict
10from .
import exceptions, utils
12logger = logging.getLogger(__name__)
18@dataclasses.dataclass(frozen=True)
27 files: list[pathlib.Path] |
None =
None
28 pg_migrations: list[pathlib.Path] |
None =
None
33 files: list[pathlib.Path]
34 pg_migrations: list[pathlib.Path]
36 def extend(self, other: ShardFiles) ->
None:
38 self.
files.extend(other.files)
39 if other.pg_migrations:
43ShardPathesDict = dict[int, ShardFileInfo]
46@dataclasses.dataclass(frozen=True)
51 files: list[pathlib.Path]
52 migrations: list[pathlib.Path]
54 def get_schema_hash(self) -> str:
55 return utils.get_files_hash(
60@dataclasses.dataclass(frozen=True)
62 service_name: str |
None
68 service_name: str |
None,
69 schema_dirs: list[pathlib.Path],
70) -> dict[str, PgShardedDatabase]:
71 """Read database schemas from directories ``schema_dirs``. ::
77 :param service_name: service name used as prefix for database name if not
78 empty, e.g. "servicename_dbname".
79 :param schema_dirs: list of pathes to scan for schemas
80 :returns: :py:class:`Dict[str, PgShardedDatabase]` where key is
81 database name as stored in :py:attr:`PgShard.dbname`
83 result: dict[str, PgShardedDatabase] = {}
84 for path
in schema_dirs:
87 schemas = _find_databases_schemas(service_name, path)
88 for dbname
in schemas.keys() & result.keys():
90 f
'Database {dbname} is declared twice',
92 result.update(schemas)
96def _find_databases_schemas(
97 service_name: str |
None,
98 schema_path: pathlib.Path,
99) -> dict[str, PgShardedDatabase]:
100 logger.debug(
'Looking up for PostgreSQL schemas at %s', schema_path)
101 shard_files_map = _build_shard_files_map(schema_path)
103 for dbname, shards
in shard_files_map.items():
104 _raise_if_invalid_shards(dbname, shards)
106 for shard_id, shard_files
in sorted(shards.items()):
110 service_name=service_name,
112 files=sorted(shard_files.files),
113 migrations=sorted(shard_files.pg_migrations),
117 service_name=service_name,
124def _build_shard_files_map(
125 root_path: pathlib.Path,
126) -> DefaultDict[str, ShardPathesDict]:
127 result: DefaultDict[str, ShardPathesDict]
128 result = collections.defaultdict(
129 lambda: collections.defaultdict(
lambda:
ShardFileInfo([], [])),
131 for shard
in _find_shard_files(root_path):
132 result[shard.name.db_name][shard.name.shard].extend(shard)
136def _find_shard_files(schema_path: pathlib.Path) -> Iterable[ShardFiles]:
137 for entry
in schema_path.iterdir():
138 shard_files = _get_shard_schema_files(entry)
139 if shard_files
is not None:
143def _get_shard_schema_files(path: pathlib.Path) -> ShardFiles |
None:
144 shard_name = _parse_shard_name(path.stem)
146 if path.suffix ==
'.sql':
149 if path.joinpath(
'migrations').is_dir():
150 return ShardFiles(shard_name, pg_migrations=[path])
151 return ShardFiles(shard_name, files=utils.scan_sql_directory(path))
155def _raise_if_invalid_shards(dbname: str, shards: ShardPathesDict) ->
None:
156 if SINGLE_SHARD
in shards:
159 'Postgresql database %s has single shard configuration '
160 'while defined as multishard' % (dbname,),
163 if set(shards.keys()) != set(range(len(shards))):
165 'Postgresql database %s is missing fixtures '
166 'for some shards' % (dbname,),
172 service_name: str |
None =
None,
173 shard_id: int = SINGLE_SHARD,
174 files: list[pathlib.Path] |
None =
None,
175 migrations: list[pathlib.Path] |
None =
None,
179 if migrations
is None:
181 if shard_id == SINGLE_SHARD:
185 actual_shard_id = shard_id
186 pretty_name =
'%s@%d' % (dbname, shard_id)
188 sharded_dbname = _database_name(service_name, dbname, shard_id)
190 shard_id=actual_shard_id,
191 pretty_name=pretty_name,
192 dbname=sharded_dbname,
194 migrations=migrations,
201def _database_name(service_name: str |
None, dbname: str, shard_id: int):
202 dbkey = (service_name, dbname)
204 if shard_id != SINGLE_SHARD:
205 suffix = f
'_{shard_id}'
207 if service_name
is not None:
208 prefix = f
'{service_name}_'
209 name = _normalize_name(prefix + dbname)
210 dbname = _normalize_name(name + suffix)
211 if len(dbname) > DB_NAME_MAX:
212 dbname = _shortened(name, suffix)
213 if dbname
not in _names_used:
214 _names_used[dbname] = dbkey
215 elif _names_used[dbname] != dbkey:
217 f
'Database name conflict for {dbkey} and {_names_used[dbname]}'
222def _shortened(name: str, suffix: str):
223 short_name =
''.join([part[:1]
for part
in name.split(
'_')])
224 name_hash = hashlib.sha1(name.encode(
'utf-8')).hexdigest()
225 hash_len = DB_NAME_MAX - len(short_name) - len(suffix) - 1
226 name_hash = name_hash[:hash_len]
227 dbname = f
'{short_name}_{name_hash}{suffix}'
228 if len(dbname) > DB_NAME_MAX:
230 f
'Dbname cannot be shortened {name}{suffix}'
235def _parse_shard_name(name) -> ShardName:
236 parts = name.rsplit(
'@', 1)
239 shard_id = int(parts[1])
240 except (ValueError, TypeError):
247def _normalize_name(name):
248 return name.replace(
'.',
'_').replace(
'-',
'_')