spark-migration 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.
- Migration/__init__.py +6 -0
- Migration/drivers/postgresql-42.7.12.jar +0 -0
- Migration/engine/__init__.py +1 -0
- Migration/engine/connect.py +72 -0
- Migration/engine/init_spark.py +30 -0
- Migration/engine/init_task.py +61 -0
- Migration/logs/__init__.py +1 -0
- Migration/logs/global_logs.py +9 -0
- Migration/logs/logs.py +4 -0
- Migration/schema/__init__.py +1 -0
- Migration/schema/control.py +101 -0
- Migration/schema/model.py +22 -0
- Migration/service/__init__.py +2 -0
- Migration/service/main.py +81 -0
- Migration/tasks/__init__.py +1 -0
- Migration/tasks/task_db.py +28 -0
- spark_migration-0.1.0.dist-info/METADATA +227 -0
- spark_migration-0.1.0.dist-info/RECORD +20 -0
- spark_migration-0.1.0.dist-info/WHEEL +5 -0
- spark_migration-0.1.0.dist-info/top_level.txt +1 -0
Migration/__init__.py
ADDED
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Infraestrutura de conexao, tarefas e Spark."""
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from sqlalchemy import create_engine, inspect, Engine
|
|
2
|
+
from Migration.logs.logs import logger
|
|
3
|
+
|
|
4
|
+
#Cria Conexão do banco de dados e retorna o nome de todas as tabelas
|
|
5
|
+
|
|
6
|
+
class Engine:
|
|
7
|
+
def __init__(self, url:str, name:str)-> None:
|
|
8
|
+
|
|
9
|
+
#Url que conecta com o banco de dados
|
|
10
|
+
self.__url = url
|
|
11
|
+
|
|
12
|
+
self.name = name
|
|
13
|
+
|
|
14
|
+
self.eng = self.con()
|
|
15
|
+
|
|
16
|
+
#Cria a engine do banco de dados
|
|
17
|
+
def con(self) -> Engine:
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
logger.info(f"Connecting to database {self.name}...")
|
|
21
|
+
|
|
22
|
+
eng = create_engine(
|
|
23
|
+
url=self.__url
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger.info(f"Connected to database {self.name}.")
|
|
27
|
+
|
|
28
|
+
return eng
|
|
29
|
+
|
|
30
|
+
except Exception as error:
|
|
31
|
+
|
|
32
|
+
raise Exception(error)
|
|
33
|
+
|
|
34
|
+
#Pega todas as tabelas do banco e retorna em uma lista
|
|
35
|
+
def tables(self) -> list:
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
logger.info(f"Discovering tables in database {self.name}...")
|
|
39
|
+
|
|
40
|
+
inspector = inspect(self.eng)
|
|
41
|
+
|
|
42
|
+
tables = inspector.get_table_names()
|
|
43
|
+
|
|
44
|
+
logger.info(
|
|
45
|
+
f"Discovered {len(tables)} table(s): "
|
|
46
|
+
f"{', '.join(tables) if tables else '[none]'}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return tables
|
|
50
|
+
|
|
51
|
+
except Exception as error:
|
|
52
|
+
|
|
53
|
+
raise Exception(error)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from Migration.logs.logs import logger
|
|
4
|
+
from pyspark.sql import SparkSession
|
|
5
|
+
|
|
6
|
+
logger.info("Starting Spark session...")
|
|
7
|
+
|
|
8
|
+
# Usa o driver que acompanha o projeto. No Windows, spark.jars.packages tenta
|
|
9
|
+
# baixar/copiar o arquivo via Hadoop e exige winutils.exe/HADOOP_HOME.
|
|
10
|
+
postgres_driver = (
|
|
11
|
+
Path(__file__).resolve().parents[1]
|
|
12
|
+
/ "drivers"
|
|
13
|
+
/ "postgresql-42.7.12.jar"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if not postgres_driver.is_file():
|
|
17
|
+
raise FileNotFoundError(f"Driver PostgreSQL nao encontrado: {postgres_driver}")
|
|
18
|
+
|
|
19
|
+
driver_path = str(postgres_driver)
|
|
20
|
+
|
|
21
|
+
app_spark = (
|
|
22
|
+
SparkSession.builder
|
|
23
|
+
.appName("Migration")
|
|
24
|
+
.master("local[*]")
|
|
25
|
+
.config("spark.driver.extraClassPath", driver_path)
|
|
26
|
+
.config("spark.executor.extraClassPath", driver_path)
|
|
27
|
+
.getOrCreate()
|
|
28
|
+
)
|
|
29
|
+
logger.info("Spark session started.")
|
|
30
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from queue import Queue
|
|
4
|
+
|
|
5
|
+
from Migration.logs.logs import logger
|
|
6
|
+
|
|
7
|
+
class Channel:
|
|
8
|
+
def __init__(self, name: str) -> None:
|
|
9
|
+
self.name = name
|
|
10
|
+
self.queue = Queue()
|
|
11
|
+
logger.info(f"Channel {self.name} created.")
|
|
12
|
+
|
|
13
|
+
def send(self, result) -> None:
|
|
14
|
+
self.queue.put(result)
|
|
15
|
+
|
|
16
|
+
def get(self):
|
|
17
|
+
return self.queue.get()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Task:
|
|
21
|
+
"""Executa tarefas em threads para compartilhar uma unica sessao Spark."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, max_workers: int, name: str, channel: Channel) -> None:
|
|
24
|
+
self.name = name
|
|
25
|
+
self.channel = channel
|
|
26
|
+
self.executor = ThreadPoolExecutor(
|
|
27
|
+
max_workers=max_workers,
|
|
28
|
+
thread_name_prefix=name,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def run(self):
|
|
32
|
+
def decorator(func):
|
|
33
|
+
@wraps(func)
|
|
34
|
+
def wrapper(*args, **kwargs):
|
|
35
|
+
future = self.executor.submit(func, *args, **kwargs)
|
|
36
|
+
|
|
37
|
+
def finished(completed):
|
|
38
|
+
try:
|
|
39
|
+
self.channel.send(completed.result())
|
|
40
|
+
except BaseException as error:
|
|
41
|
+
self.channel.send(error)
|
|
42
|
+
|
|
43
|
+
future.add_done_callback(finished)
|
|
44
|
+
return future
|
|
45
|
+
|
|
46
|
+
return wrapper
|
|
47
|
+
|
|
48
|
+
return decorator
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Canal onde vai receber o resultado das tasks.
|
|
52
|
+
ch = Channel(name="Migrations")
|
|
53
|
+
|
|
54
|
+
# Threads evitam criar uma JVM/SparkSession por tabela e excecoes nao precisam
|
|
55
|
+
# ser serializadas entre processos.
|
|
56
|
+
task = Task(
|
|
57
|
+
max_workers=4,
|
|
58
|
+
name="Migration_task",
|
|
59
|
+
channel=ch
|
|
60
|
+
)
|
|
61
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuracao de logs do framework."""
|
Migration/logs/logs.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Modelos e controle da migracao de dados."""
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from Migration.logs.logs import logger
|
|
2
|
+
from Migration.engine.init_spark import app_spark
|
|
3
|
+
from pyspark.sql import DataFrame
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
#Essa classe serve para ler e transferir a tabela
|
|
7
|
+
class SparkDb:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
url: str,
|
|
11
|
+
table_name: str,
|
|
12
|
+
new_url: str,
|
|
13
|
+
user: str,
|
|
14
|
+
password: str,
|
|
15
|
+
new_user: str,
|
|
16
|
+
new_password: str,
|
|
17
|
+
) -> None:
|
|
18
|
+
|
|
19
|
+
self.url = url
|
|
20
|
+
self.table = table_name
|
|
21
|
+
self.spark = app_spark
|
|
22
|
+
self.new_url = new_url
|
|
23
|
+
self.user = user
|
|
24
|
+
self.password = password
|
|
25
|
+
self.new_user = new_user
|
|
26
|
+
self.new_password = new_password
|
|
27
|
+
|
|
28
|
+
#Le a tabela
|
|
29
|
+
def read(self, max_attempts: int = 3)-> DataFrame:
|
|
30
|
+
for attempt in range(1, max_attempts + 1):
|
|
31
|
+
try:
|
|
32
|
+
logger.info(
|
|
33
|
+
f"Reading table {self.table} "
|
|
34
|
+
f"(attempt {attempt}/{max_attempts})"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
df = (self.spark.
|
|
38
|
+
read.format("jdbc").
|
|
39
|
+
option("url", self.url).
|
|
40
|
+
option("dbtable", self.table).
|
|
41
|
+
option("driver", "org.postgresql.Driver").
|
|
42
|
+
option("user", self.user).
|
|
43
|
+
option("password", self.password).
|
|
44
|
+
option("connectTimeout", "30").
|
|
45
|
+
option("socketTimeout", "300").
|
|
46
|
+
option("tcpKeepAlive", "true").
|
|
47
|
+
load())
|
|
48
|
+
|
|
49
|
+
logger.info(f"Table {self.table} loaded.")
|
|
50
|
+
return df
|
|
51
|
+
|
|
52
|
+
except Exception as error:
|
|
53
|
+
if attempt == max_attempts:
|
|
54
|
+
raise RuntimeError(
|
|
55
|
+
f"Nao foi possivel ler a tabela {self.table} "
|
|
56
|
+
f"apos {max_attempts} tentativas: {error}"
|
|
57
|
+
) from error
|
|
58
|
+
|
|
59
|
+
logger.warning(
|
|
60
|
+
f"Temporary failure while reading {self.table}: {error}. "
|
|
61
|
+
"Retrying..."
|
|
62
|
+
)
|
|
63
|
+
time.sleep(3 * attempt)
|
|
64
|
+
|
|
65
|
+
#Salva no novo banco de dados
|
|
66
|
+
def save(self) -> None:
|
|
67
|
+
try:
|
|
68
|
+
logger.info(f"Writing table {self.table}...")
|
|
69
|
+
|
|
70
|
+
(self.df.write.
|
|
71
|
+
format("jdbc").
|
|
72
|
+
option("url", self.new_url).
|
|
73
|
+
option("dbtable", self.table).
|
|
74
|
+
option("driver", "org.postgresql.Driver").
|
|
75
|
+
option("user", self.new_user).
|
|
76
|
+
option("password", self.new_password).
|
|
77
|
+
mode("append").save()
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
logger.info(f"Table {self.table} written successfully.")
|
|
81
|
+
|
|
82
|
+
except Exception as error:
|
|
83
|
+
raise Exception(error)
|
|
84
|
+
|
|
85
|
+
def run(self) -> str:
|
|
86
|
+
self.df = self.read()
|
|
87
|
+
self.save()
|
|
88
|
+
return self.table
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from urllib.parse import quote_plus
|
|
2
|
+
#serve pra formatar od dados
|
|
3
|
+
class Formate:
|
|
4
|
+
|
|
5
|
+
def __init__(self, host:str, port:int, user:str, password:str, dbname:str)->None:
|
|
6
|
+
self.host = host
|
|
7
|
+
self.port = port
|
|
8
|
+
self.user = user
|
|
9
|
+
self.Pass = password
|
|
10
|
+
self.db = dbname
|
|
11
|
+
|
|
12
|
+
#formata os dados de conexão pra url do spark
|
|
13
|
+
def url_spark(self) -> str:
|
|
14
|
+
return f"jdbc:postgresql://{self.host}:{self.port}/{self.db}"
|
|
15
|
+
|
|
16
|
+
#forma para url do sqlalchemy
|
|
17
|
+
def url_alchemy(self) ->str:
|
|
18
|
+
password = quote_plus(self.Pass)
|
|
19
|
+
|
|
20
|
+
return f"postgresql://{self.user}:{password}@{self.host}:{self.port}/{self.db}"
|
|
21
|
+
|
|
22
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from Migration.logs.logs import logger
|
|
2
|
+
from Migration.engine.connect import Engine
|
|
3
|
+
from Migration.tasks.task_db import SaveDb
|
|
4
|
+
from Migration.engine.init_task import ch
|
|
5
|
+
from Migration.schema.model import Formate
|
|
6
|
+
|
|
7
|
+
#Faz toda migração do banco de dados
|
|
8
|
+
class Migration:
|
|
9
|
+
def __init__(self, data:dict, new_data:dict)->None:
|
|
10
|
+
|
|
11
|
+
self.__url = Formate(
|
|
12
|
+
host=data["host"],
|
|
13
|
+
port=data["port"],
|
|
14
|
+
dbname=data["dbname"],
|
|
15
|
+
password=data["password"],
|
|
16
|
+
user=data["user"]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
self.__newurl = Formate(
|
|
20
|
+
host=new_data["host"],
|
|
21
|
+
port=new_data["port"],
|
|
22
|
+
dbname=new_data["dbname"],
|
|
23
|
+
password=new_data["password"],
|
|
24
|
+
user=new_data["user"]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
self.__source_user = data["user"]
|
|
28
|
+
self.__source_password = data["password"]
|
|
29
|
+
self.__destination_user = new_data["user"]
|
|
30
|
+
self.__destination_password = new_data["password"]
|
|
31
|
+
|
|
32
|
+
self.tables = Engine(url=self.__url.url_alchemy(), name=data["dbname"]).tables()
|
|
33
|
+
|
|
34
|
+
self.channel = ch
|
|
35
|
+
|
|
36
|
+
#Incia uma task para cada tabela do banco de dados
|
|
37
|
+
def init_task(self)->None:
|
|
38
|
+
for table in self.tables:
|
|
39
|
+
SaveDb(
|
|
40
|
+
table_name=table,
|
|
41
|
+
url=self.__url.url_spark(),
|
|
42
|
+
new_url=self.__newurl.url_spark(),
|
|
43
|
+
user=self.__source_user,
|
|
44
|
+
password=self.__source_password,
|
|
45
|
+
new_user=self.__destination_user,
|
|
46
|
+
new_password=self.__destination_password,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
#Chama o metodo init_task para iniciar as tasks, e depois entra num loop e espera todas as tasks finalizarem
|
|
50
|
+
def run(self) -> None:
|
|
51
|
+
logger.info("Starting database migration...")
|
|
52
|
+
|
|
53
|
+
self.init_task()
|
|
54
|
+
|
|
55
|
+
tables_finish = []
|
|
56
|
+
|
|
57
|
+
while len(tables_finish) != len(self.tables):
|
|
58
|
+
|
|
59
|
+
data = self.channel.get()
|
|
60
|
+
|
|
61
|
+
if isinstance(data, BaseException):
|
|
62
|
+
raise RuntimeError(f"Falha durante a migracao: {data}") from data
|
|
63
|
+
|
|
64
|
+
if data is not None:
|
|
65
|
+
logger.info(f"Table {data} migrated successfully.")
|
|
66
|
+
tables_finish.append(data)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
logger.info("Database migration completed successfully.")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tarefas de migracao por tabela."""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
from Migration.engine.init_task import task
|
|
3
|
+
from Migration.schema.control import SparkDb
|
|
4
|
+
|
|
5
|
+
#Transforma todo sparkdb em uma task
|
|
6
|
+
@task.run()
|
|
7
|
+
def SaveDb(
|
|
8
|
+
url: str,
|
|
9
|
+
new_url: str,
|
|
10
|
+
table_name: str,
|
|
11
|
+
user: str,
|
|
12
|
+
password: str,
|
|
13
|
+
new_user: str,
|
|
14
|
+
new_password: str,
|
|
15
|
+
):
|
|
16
|
+
tb = SparkDb(
|
|
17
|
+
url=url,
|
|
18
|
+
new_url=new_url,
|
|
19
|
+
table_name=table_name,
|
|
20
|
+
user=user,
|
|
21
|
+
password=password,
|
|
22
|
+
new_user=new_user,
|
|
23
|
+
new_password=new_password,
|
|
24
|
+
).run()
|
|
25
|
+
return tb
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spark_migration
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Spark Migration: framework concorrente para migracao de dados PostgreSQL com Apache Spark.
|
|
5
|
+
Keywords: database,migration,postgresql,pyspark,spark,etl
|
|
6
|
+
Classifier: Development Status :: 4 - Beta
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Database
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: pyspark<5,>=4.0
|
|
20
|
+
Requires-Dist: SQLAlchemy<3,>=2.0
|
|
21
|
+
Requires-Dist: psycopg2-binary<3,>=2.9
|
|
22
|
+
|
|
23
|
+
<div align="center">
|
|
24
|
+
|
|
25
|
+
# Spark Migration
|
|
26
|
+
|
|
27
|
+
### Um framework concorrente para migração de dados PostgreSQL com Apache Spark
|
|
28
|
+
|
|
29
|
+
[](https://www.python.org/)
|
|
30
|
+
[](https://spark.apache.org/)
|
|
31
|
+
[](https://www.postgresql.org/)
|
|
32
|
+
[](#status-do-projeto)
|
|
33
|
+
|
|
34
|
+
Descubra tabelas, execute tarefas concorrentes e transfira dados via JDBC usando uma única sessão Spark compartilhada.
|
|
35
|
+
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Visão geral
|
|
41
|
+
|
|
42
|
+
Spark Migration é um framework Python para copiar dados entre bancos PostgreSQL. Ele combina SQLAlchemy para inspecionar o banco de origem, Apache Spark para transferir os dados via JDBC e um conjunto controlado de threads para processar as tabelas concorrentemente.
|
|
43
|
+
|
|
44
|
+
A API pública foi projetada para ser simples:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
Migration(data=origem, new_data=destino).run()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Recursos
|
|
51
|
+
|
|
52
|
+
- Descoberta automática das tabelas do banco de origem.
|
|
53
|
+
- Leitura e escrita via JDBC com Apache Spark.
|
|
54
|
+
- Execução concorrente de até quatro tabelas.
|
|
55
|
+
- Uma única sessão Spark compartilhada entre todas as tarefas.
|
|
56
|
+
- Até três tentativas para falhas temporárias de leitura.
|
|
57
|
+
- Propagação de erros das tarefas para a thread principal.
|
|
58
|
+
- Driver JDBC do PostgreSQL incluído no pacote.
|
|
59
|
+
- Logs estruturados em inglês.
|
|
60
|
+
|
|
61
|
+
## Como funciona
|
|
62
|
+
|
|
63
|
+
```mermaid
|
|
64
|
+
flowchart LR
|
|
65
|
+
A["PostgreSQL de origem"] -->|"Descobre tabelas"| B["SQLAlchemy"]
|
|
66
|
+
B --> C["Serviço Spark Migration"]
|
|
67
|
+
C -->|"Uma tarefa por tabela"| D["Pool de threads"]
|
|
68
|
+
D --> E["Sessão Spark compartilhada"]
|
|
69
|
+
E -->|"Leitura JDBC"| A
|
|
70
|
+
E -->|"Escrita JDBC em append"| F["PostgreSQL de destino"]
|
|
71
|
+
D -->|"Resultado ou erro"| C
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
1. `Migration` formata as configurações de conexão da origem e do destino.
|
|
75
|
+
2. O SQLAlchemy conecta-se à origem e descobre suas tabelas.
|
|
76
|
+
3. O serviço cria uma tarefa para cada tabela encontrada.
|
|
77
|
+
4. Cada tarefa lê sua tabela em um DataFrame Spark via JDBC.
|
|
78
|
+
5. O Spark adiciona o DataFrame à tabela correspondente no destino.
|
|
79
|
+
6. A thread principal aguarda os resultados e interrompe a execução quando uma tarefa falha.
|
|
80
|
+
|
|
81
|
+
## Requisitos
|
|
82
|
+
|
|
83
|
+
- Python 3.10 ou superior.
|
|
84
|
+
- Java 17 disponível no `PATH`.
|
|
85
|
+
- Bancos PostgreSQL de origem e destino.
|
|
86
|
+
- Acesso de rede aos dois bancos.
|
|
87
|
+
- Permissão de leitura nas tabelas de origem.
|
|
88
|
+
- Permissões de criação e inserção no destino.
|
|
89
|
+
|
|
90
|
+
## Instalação
|
|
91
|
+
|
|
92
|
+
Instale o Spark Migration e suas dependências diretamente pelo PyPI:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install spark_migration
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Após a instalação, o framework é importado como `Migration`.
|
|
99
|
+
|
|
100
|
+
## Uso rápido
|
|
101
|
+
|
|
102
|
+
Mantenha as credenciais fora do código-fonte. Este exemplo utiliza variáveis de ambiente:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import os
|
|
106
|
+
|
|
107
|
+
from Migration import Migration
|
|
108
|
+
|
|
109
|
+
origem = {
|
|
110
|
+
"host": os.environ["SOURCE_DB_HOST"],
|
|
111
|
+
"port": int(os.getenv("SOURCE_DB_PORT", "5432")),
|
|
112
|
+
"dbname": os.environ["SOURCE_DB_NAME"],
|
|
113
|
+
"user": os.environ["SOURCE_DB_USER"],
|
|
114
|
+
"password": os.environ["SOURCE_DB_PASSWORD"],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
destino = {
|
|
118
|
+
"host": os.environ["DESTINATION_DB_HOST"],
|
|
119
|
+
"port": int(os.getenv("DESTINATION_DB_PORT", "5432")),
|
|
120
|
+
"dbname": os.environ["DESTINATION_DB_NAME"],
|
|
121
|
+
"user": os.environ["DESTINATION_DB_USER"],
|
|
122
|
+
"password": os.environ["DESTINATION_DB_PASSWORD"],
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
Migration(data=origem, new_data=destino).run()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Execute o arquivo normalmente:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
python spark_migration.py
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Exemplo de saída:
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
INFO | Starting Spark session...
|
|
138
|
+
INFO | Spark session started.
|
|
139
|
+
INFO | Connecting to database postgres...
|
|
140
|
+
INFO | Discovered 1 table(s): orders
|
|
141
|
+
INFO | Starting database migration...
|
|
142
|
+
INFO | Reading table orders (attempt 1/3)
|
|
143
|
+
INFO | Table orders written successfully.
|
|
144
|
+
INFO | Database migration completed successfully.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Configuração da conexão
|
|
148
|
+
|
|
149
|
+
Os parâmetros `data` e `new_data` recebem os mesmos campos:
|
|
150
|
+
|
|
151
|
+
| Campo | Tipo | Descrição |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| `host` | `str` | Endereço ou IP do servidor PostgreSQL |
|
|
154
|
+
| `port` | `int` | Porta do PostgreSQL, normalmente `5432` |
|
|
155
|
+
| `dbname` | `str` | Nome do banco de dados |
|
|
156
|
+
| `user` | `str` | Usuário do banco |
|
|
157
|
+
| `password` | `str` | Senha do banco |
|
|
158
|
+
|
|
159
|
+
## Comportamento da escrita
|
|
160
|
+
|
|
161
|
+
A versão atual utiliza o modo `append` do Spark.
|
|
162
|
+
|
|
163
|
+
- Se a tabela não existir no destino, o Spark a criará com base no DataFrame.
|
|
164
|
+
- Se a tabela já existir, os novos registros serão adicionados.
|
|
165
|
+
- Executar a mesma migração mais de uma vez pode duplicar os dados.
|
|
166
|
+
- Uma falha durante a escrita pode deixar dados parciais no destino.
|
|
167
|
+
|
|
168
|
+
Utilize um destino limpo para migrações completas e compare a quantidade de registros na origem e no destino antes de repetir uma execução.
|
|
169
|
+
|
|
170
|
+
## Escopo e limitações atuais
|
|
171
|
+
|
|
172
|
+
O framework atualmente migra os registros das tabelas. Ele ainda não reproduz todos os objetos e configurações do PostgreSQL, incluindo:
|
|
173
|
+
|
|
174
|
+
- chaves primárias e estrangeiras;
|
|
175
|
+
- índices e restrições únicas;
|
|
176
|
+
- sequências e seus valores atuais;
|
|
177
|
+
- views e materialized views;
|
|
178
|
+
- triggers e funções armazenadas;
|
|
179
|
+
- usuários, permissões e políticas de segurança em nível de linha;
|
|
180
|
+
- extensões e configurações do banco.
|
|
181
|
+
|
|
182
|
+
Em migrações de produção, recrie e valide esses objetos separadamente.
|
|
183
|
+
|
|
184
|
+
## Observações para Windows
|
|
185
|
+
|
|
186
|
+
O Spark pode informar que `winutils.exe`, `HADOOP_HOME` ou a biblioteca nativa do Hadoop não estão disponíveis. O framework carrega diretamente o driver JDBC incluído no pacote, portanto esses avisos não impedem o fluxo suportado de migração PostgreSQL.
|
|
187
|
+
|
|
188
|
+
Se o Spark não iniciar, confirme se o Java 17 está instalado:
|
|
189
|
+
|
|
190
|
+
```powershell
|
|
191
|
+
java -version
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Solução de problemas
|
|
195
|
+
|
|
196
|
+
### `Connection reset`
|
|
197
|
+
|
|
198
|
+
O banco ou o gerenciador de conexões encerrou a conexão JDBC. O framework repete automaticamente a leitura até três vezes. Se o problema continuar, verifique o estado do banco, a estabilidade da rede, os requisitos de SSL e os limites do pool de conexões.
|
|
199
|
+
|
|
200
|
+
### `Migration failed`
|
|
201
|
+
|
|
202
|
+
A thread principal recebeu uma exceção de uma tarefa. Procure o primeiro erro de banco ou Spark exibido antes dessa mensagem; normalmente ele contém a causa real.
|
|
203
|
+
|
|
204
|
+
### Porta da interface Spark ocupada
|
|
205
|
+
|
|
206
|
+
O Spark pode selecionar outra porta quando a porta local `4040` já estiver em uso. Isso é apenas informativo e normalmente não impede a migração.
|
|
207
|
+
|
|
208
|
+
## Segurança
|
|
209
|
+
|
|
210
|
+
- Nunca inclua senhas de banco ou tokens do PyPI no repositório.
|
|
211
|
+
- Prefira variáveis de ambiente ou um gerenciador de segredos.
|
|
212
|
+
- Utilize usuários de banco dedicados e com as menores permissões necessárias.
|
|
213
|
+
- Troque credenciais que já tenham sido publicadas ou compartilhadas.
|
|
214
|
+
|
|
215
|
+
## Status do projeto
|
|
216
|
+
|
|
217
|
+
Spark Migration está em fase **Beta**. A migração de registros entre bancos PostgreSQL está funcional. Modos de escrita idempotentes, migração completa do esquema, validação automática e suporte a outros bancos ainda estão em desenvolvimento.
|
|
218
|
+
|
|
219
|
+
## Roadmap
|
|
220
|
+
|
|
221
|
+
- Modos configuráveis `append`, `overwrite` e falha se a tabela existir.
|
|
222
|
+
- Validação por quantidade de registros e checksum.
|
|
223
|
+
- Migração de esquema, restrições, índices e sequências.
|
|
224
|
+
- Filtros para incluir ou excluir tabelas.
|
|
225
|
+
- Configuração de concorrência e tentativas.
|
|
226
|
+
- Testes automatizados e integração contínua.
|
|
227
|
+
- Suporte a outros bancos compatíveis com JDBC.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Migration/__init__.py,sha256=JtzEERalzJ2xAzBMpj1jlRT1d81Yv1mvX9EQZUWsglQ,145
|
|
2
|
+
Migration/drivers/postgresql-42.7.12.jar,sha256=Mfv28GsiF_tR1RAM7lGyJiXMgWQNoGebR5FOVMHmN3w,1142967
|
|
3
|
+
Migration/engine/__init__.py,sha256=ajUXJJV8VDSRl7qc7OipiHsNMNRNNNQHuLZqMpd9pfo,50
|
|
4
|
+
Migration/engine/connect.py,sha256=uy868FKvtDQ3S8Kj3qG80E034e_0lt5bSPqx_vi67Cg,1668
|
|
5
|
+
Migration/engine/init_spark.py,sha256=WjOui1-iTVnA4t8Y9dSj8pU9BuGe49rotGg9eF8cf1A,824
|
|
6
|
+
Migration/engine/init_task.py,sha256=ncXzjJR4eWdvuRNMULPGSi3VLFAA8W916laXb-FOP0g,1611
|
|
7
|
+
Migration/logs/__init__.py,sha256=iQGKX8Zm99lSQHX0WBxs6Q7y7qc4md3f3r08doBvaXo,41
|
|
8
|
+
Migration/logs/global_logs.py,sha256=6Ao5j5slSVlWc-onCNYhtwmyKYt3TdnBmsCGVi8A3YY,227
|
|
9
|
+
Migration/logs/logs.py,sha256=bseukWs_zC7PlQNefACYQ6B0mZ-ewHBiMGt8mPlZSi4,131
|
|
10
|
+
Migration/schema/__init__.py,sha256=_0HAthGwzgZtiSQavueq9k2HPN-_PIbEGLq-AzxdxV4,47
|
|
11
|
+
Migration/schema/control.py,sha256=PQf9gwuyuMchhwcNaUMi8EVWIfgLy5FPbgV_f2zcmTw,3017
|
|
12
|
+
Migration/schema/model.py,sha256=QpzLfS7hd5_0pSAzLH3_Dyt5v3Yf8QHpa2qWdGZjoro,710
|
|
13
|
+
Migration/service/__init__.py,sha256=lWYZMmb44ppJCxyT8F0rNmCq_uxlnYz-PO21WL94AVI,79
|
|
14
|
+
Migration/service/main.py,sha256=cBSW0cbDma2_qk0r5q3QuL1QCMcyvLjN72e8MD25bRI,2544
|
|
15
|
+
Migration/tasks/__init__.py,sha256=m-u01YlHrlaPH_bXt2Oif7zGmJYv2iXJtCg-DnvP8WM,38
|
|
16
|
+
Migration/tasks/task_db.py,sha256=o3MultPt5heTUB-AYV4NHEOpG3miT-kP8guMkBOlTOk,524
|
|
17
|
+
spark_migration-0.1.0.dist-info/METADATA,sha256=kftNcLML8L7hJ5VHundpIF3bqTinJo_uZgD6dmLx-AA,8692
|
|
18
|
+
spark_migration-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
19
|
+
spark_migration-0.1.0.dist-info/top_level.txt,sha256=1osF59DZ2sfzNrY8VhBFNkNEscA2EUSgbJ7h7HsSHkc,10
|
|
20
|
+
spark_migration-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Migration
|