Skip to content

Commit cae79b8

Browse files
DanielResgateTIRomularquesdevin-ai-integration[bot]
authored
Merge pull request #11 from EluminiIT/devin/1774905208-adapt-sqlserver
* feat: adapt project from PostgreSQL to SQL Server - Replace psycopg driver with pyodbc in pyproject.toml - Update config.py: POSTGRES_* env vars -> MSSQL_*, connection string to mssql+pyodbc - Update compose.yml and compose.override.yml: mcr.microsoft.com/mssql/server:2022-latest - Update Dockerfile: install Microsoft ODBC Driver 18 for SQL Server - Recreate Alembic migrations from scratch for MSSQL compatibility (single initial migration) - Delete all old PostgreSQL-specific migrations (uuid-ossp extension, postgresql.UUID, etc.) - Update GitHub Actions workflows (deploy-staging, deploy-production, test-backend) - Update test-backend workflow with ODBC driver install, SQL Server wait, and DB creation steps - Update copier.yml: postgres_password -> mssql_password - Fix test conftest.py: add AuditLog and CompanyInvite cleanup for FK constraint order - Update uv.lock with new dependencies Note: .env must be manually updated - replace POSTGRES_* vars with: MSSQL_SERVER=localhost MSSQL_PORT=1433 MSSQL_DB=app MSSQL_USER=sa MSSQL_PASSWORD=<strong_password> MSSQL_DRIVER=ODBC Driver 18 for SQL Server Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: address CI failures and Devin Review comments - Add URL encoding (quote_plus) for MSSQL user/password in connection string (config.py) - Create scripts/ci-generate-env.sh to generate .env with MSSQL vars for CI - Update test-backend.yml: use ci-generate-env.sh, fix gpg --batch --yes flag - Update test-docker-compose.yml: use ci-generate-env.sh for .env generation - Update playwright.yml: use ci-generate-env.sh for .env generation - Merge master to include PR #10 fix Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: add missing env vars (FRONTEND_HOST, DOCKER_IMAGE_*) to CI env script - Add FRONTEND_HOST, SMTP_TLS, SMTP_SSL, SMTP_PORT, DOCKER_IMAGE_BACKEND, DOCKER_IMAGE_FRONTEND to ci-generate-env.sh - Add 'Generate .env for CI' step to pre-commit.yml (fixes MSSQL_SERVER/MSSQL_USER validation errors) - Move .env generation before generate-client.sh in playwright.yml (script needs MSSQL env vars) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: move USER_ROLE_LABELS/USER_MANAGER_ROLES to non-generated constants file These constants were manually added to types.gen.ts which gets overwritten by generate-client.sh. Moved to frontend/src/lib/user-constants.ts and updated all imports. Also applied lint auto-fixes. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: add Changethis1! to default secret check (Devin Review) The MSSQL default password Changethis1! was not caught by the _check_default_secret security guard, allowing deployments with default credentials. Now checks both 'changethis' and 'Changethis1!'. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: add SQL Server readiness timeout in CI workflow Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: update .env from POSTGRES_* to MSSQL_* variables for SQL Server migration The .env file still had old POSTGRES_* variables which would break local development and docker compose since config.py now expects MSSQL_* variables. This matches the variables used in compose.yml and backend/app/core/config.py. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Romulo Marques <romulo.marques@elumini-it.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2 parents 9a91b86 + 1953065 commit cae79b8

32 files changed

+418
-674
lines changed

.env

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ SMTP_TLS=True
3131
SMTP_SSL=False
3232
SMTP_PORT=587
3333

34-
# Postgres
35-
POSTGRES_SERVER=localhost
36-
POSTGRES_PORT=5432
37-
POSTGRES_DB=app
38-
POSTGRES_USER=postgres
39-
POSTGRES_PASSWORD=changethis
34+
# SQL Server
35+
MSSQL_SERVER=localhost
36+
MSSQL_PORT=1433
37+
MSSQL_DB=app
38+
MSSQL_USER=sa
39+
MSSQL_PASSWORD=Changethis1!
40+
MSSQL_DRIVER=ODBC Driver 18 for SQL Server
4041

4142
SENTRY_DSN=
4243

.github/workflows/deploy-production.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ jobs:
2323
SMTP_USER: ${{ secrets.SMTP_USER }}
2424
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
2525
EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }}
26-
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
26+
MSSQL_PASSWORD: ${{ secrets.MSSQL_PASSWORD }}
27+
MSSQL_SERVER: ${{ secrets.MSSQL_SERVER }}
28+
MSSQL_PORT: ${{ secrets.MSSQL_PORT }}
29+
MSSQL_DB: ${{ secrets.MSSQL_DB }}
30+
MSSQL_USER: ${{ secrets.MSSQL_USER }}
31+
MSSQL_DRIVER: ${{ secrets.MSSQL_DRIVER }}
2732
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
2833
steps:
2934
- name: Checkout

.github/workflows/deploy-staging.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ jobs:
2323
SMTP_USER: ${{ secrets.SMTP_USER }}
2424
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
2525
EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }}
26-
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
26+
MSSQL_PASSWORD: ${{ secrets.MSSQL_PASSWORD }}
27+
MSSQL_SERVER: ${{ secrets.MSSQL_SERVER }}
28+
MSSQL_PORT: ${{ secrets.MSSQL_PORT }}
29+
MSSQL_DB: ${{ secrets.MSSQL_DB }}
30+
MSSQL_USER: ${{ secrets.MSSQL_USER }}
31+
MSSQL_DRIVER: ${{ secrets.MSSQL_DRIVER }}
2732
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
2833
steps:
2934
- name: Checkout

.github/workflows/playwright.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ jobs:
6363
working-directory: backend
6464
- run: bun ci
6565
working-directory: frontend
66+
- name: Generate .env for CI
67+
run: bash scripts/ci-generate-env.sh db
6668
- run: bash scripts/generate-client.sh
6769
- run: docker compose build
6870
- run: docker compose down -v --remove-orphans

.github/workflows/pre-commit.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ jobs:
4949
requirements**.txt
5050
pyproject.toml
5151
uv.lock
52+
- name: Generate .env for CI
53+
run: bash scripts/ci-generate-env.sh localhost
5254
- name: Install backend dependencies
5355
run: uv sync --all-packages
5456
- name: Install frontend dependencies

.github/workflows/test-backend.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,27 @@ jobs:
2121
python-version: "3.10"
2222
- name: Install uv
2323
uses: astral-sh/setup-uv@v7
24+
- name: Install ODBC Driver 18 for SQL Server
25+
run: |
26+
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
27+
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/$(lsb_release -rs)/prod $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/mssql-release.list
28+
sudo apt-get update
29+
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
30+
- name: Generate .env for CI
31+
run: bash scripts/ci-generate-env.sh localhost
2432
- run: docker compose down -v --remove-orphans
2533
- run: docker compose up -d db mailcatcher
34+
- name: Wait for SQL Server to be ready
35+
run: |
36+
for i in $(seq 1 30); do
37+
docker compose exec db /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "${MSSQL_PASSWORD}" -Q "SELECT 1" -C > /dev/null 2>&1 && exit 0
38+
echo "Waiting for SQL Server... ($i/30)"
39+
sleep 2
40+
done
41+
echo "SQL Server did not become ready in time" && exit 1
42+
- name: Create database
43+
run: |
44+
docker compose exec db /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "${MSSQL_PASSWORD}" -Q "IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = '${MSSQL_DB}') CREATE DATABASE [${MSSQL_DB}]" -C
2645
- name: Migrate DB
2746
run: uv run bash scripts/prestart.sh
2847
working-directory: backend

.github/workflows/test-docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ jobs:
1616
steps:
1717
- name: Checkout
1818
uses: actions/checkout@v6
19+
- name: Generate .env for CI
20+
run: bash scripts/ci-generate-env.sh db
1921
- run: docker compose build
2022
- run: docker compose down -v --remove-orphans
2123
- run: docker compose up -d --wait backend frontend adminer

backend/Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ FROM python:3.10
22

33
ENV PYTHONUNBUFFERED=1
44

5+
# Install Microsoft ODBC Driver 18 for SQL Server
6+
RUN apt-get update && \
7+
apt-get install -y --no-install-recommends curl gnupg apt-transport-https && \
8+
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg && \
9+
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/debian/12/prod bookworm main" > /etc/apt/sources.list.d/mssql-release.list && \
10+
apt-get update && \
11+
ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql18 unixodbc-dev && \
12+
apt-get clean && \
13+
rm -rf /var/lib/apt/lists/*
14+
515
# Install uv
616
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
717
COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""Initial schema for SQL Server
2+
3+
Revision ID: 0001
4+
Revises:
5+
Create Date: 2026-03-30 21:00:00.000000
6+
7+
"""
8+
import sqlalchemy as sa
9+
import sqlmodel.sql.sqltypes
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "0001"
14+
down_revision = None
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# Create user table
21+
op.create_table(
22+
"user",
23+
sa.Column("email", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
24+
sa.Column("is_active", sa.Boolean(), nullable=False),
25+
sa.Column("is_superuser", sa.Boolean(), nullable=False),
26+
sa.Column("full_name", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
27+
sa.Column(
28+
"role",
29+
sqlmodel.sql.sqltypes.AutoString(),
30+
nullable=False,
31+
server_default="comercial",
32+
),
33+
sa.Column("id", sa.Uuid(), nullable=False),
34+
sa.Column("hashed_password", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
35+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
36+
sa.PrimaryKeyConstraint("id"),
37+
)
38+
op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True)
39+
40+
# Create item table
41+
op.create_table(
42+
"item",
43+
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
44+
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
45+
sa.Column("id", sa.Uuid(), nullable=False),
46+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
47+
sa.Column("owner_id", sa.Uuid(), nullable=False),
48+
sa.ForeignKeyConstraint(["owner_id"], ["user.id"], ondelete="CASCADE"),
49+
sa.PrimaryKeyConstraint("id"),
50+
)
51+
52+
# Create company table
53+
op.create_table(
54+
"company",
55+
sa.Column("cnpj", sqlmodel.sql.sqltypes.AutoString(length=20), nullable=False),
56+
sa.Column("razao_social", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
57+
sa.Column("representante_legal", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
58+
sa.Column("data_abertura", sa.Date(), nullable=True),
59+
sa.Column("nome_fantasia", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
60+
sa.Column("porte", sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True),
61+
sa.Column("atividade_economica_principal", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
62+
sa.Column("atividade_economica_secundaria", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
63+
sa.Column("natureza_juridica", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
64+
sa.Column("logradouro", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
65+
sa.Column("numero", sqlmodel.sql.sqltypes.AutoString(length=20), nullable=True),
66+
sa.Column("complemento", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
67+
sa.Column("cep", sqlmodel.sql.sqltypes.AutoString(length=10), nullable=True),
68+
sa.Column("bairro", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
69+
sa.Column("municipio", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
70+
sa.Column("uf", sqlmodel.sql.sqltypes.AutoString(length=2), nullable=True),
71+
sa.Column("endereco_eletronico", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
72+
sa.Column("telefone_comercial", sqlmodel.sql.sqltypes.AutoString(length=20), nullable=True),
73+
sa.Column("situacao_cadastral", sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True),
74+
sa.Column("data_situacao_cadastral", sa.Date(), nullable=True),
75+
sa.Column("cpf_representante_legal", sqlmodel.sql.sqltypes.AutoString(length=14), nullable=True),
76+
sa.Column("identidade_representante_legal", sqlmodel.sql.sqltypes.AutoString(length=20), nullable=True),
77+
sa.Column("logradouro_representante_legal", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
78+
sa.Column("numero_representante_legal", sqlmodel.sql.sqltypes.AutoString(length=20), nullable=True),
79+
sa.Column("complemento_representante_legal", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
80+
sa.Column("cep_representante_legal", sqlmodel.sql.sqltypes.AutoString(length=10), nullable=True),
81+
sa.Column("bairro_representante_legal", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
82+
sa.Column("municipio_representante_legal", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
83+
sa.Column("uf_representante_legal", sqlmodel.sql.sqltypes.AutoString(length=2), nullable=True),
84+
sa.Column("endereco_eletronico_representante_legal", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
85+
sa.Column("telefones_representante_legal", sqlmodel.sql.sqltypes.AutoString(length=40), nullable=True),
86+
sa.Column("data_nascimento_representante_legal", sa.Date(), nullable=True),
87+
sa.Column("banco_cc_cnpj", sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True),
88+
sa.Column("agencia_cc_cnpj", sqlmodel.sql.sqltypes.AutoString(length=20), nullable=True),
89+
sa.Column("email", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
90+
sa.Column(
91+
"status",
92+
sqlmodel.sql.sqltypes.AutoString(),
93+
nullable=False,
94+
server_default="completed",
95+
),
96+
sa.Column("id", sa.Uuid(), nullable=False),
97+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
98+
sa.PrimaryKeyConstraint("id"),
99+
)
100+
op.create_index(op.f("ix_company_cnpj"), "company", ["cnpj"], unique=True)
101+
102+
# Create companyinvite table
103+
op.create_table(
104+
"companyinvite",
105+
sa.Column("email", sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
106+
sa.Column("id", sa.Uuid(), nullable=False),
107+
sa.Column("company_id", sa.Uuid(), nullable=False),
108+
sa.Column("token", sqlmodel.sql.sqltypes.AutoString(length=500), nullable=False),
109+
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
110+
sa.Column("used", sa.Boolean(), nullable=False, server_default=sa.text("0")),
111+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
112+
sa.ForeignKeyConstraint(["company_id"], ["company.id"], ondelete="CASCADE"),
113+
sa.PrimaryKeyConstraint("id"),
114+
)
115+
op.create_index(op.f("ix_companyinvite_token"), "companyinvite", ["token"], unique=True)
116+
117+
# Create auditlog table
118+
op.create_table(
119+
"auditlog",
120+
sa.Column("id", sa.Uuid(), nullable=False),
121+
sa.Column(
122+
"action",
123+
sqlmodel.sql.sqltypes.AutoString(),
124+
nullable=False,
125+
),
126+
sa.Column("target_user_id", sa.Uuid(), nullable=False),
127+
sa.Column("performed_by_id", sa.Uuid(), nullable=False),
128+
sa.Column(
129+
"changes",
130+
sqlmodel.sql.sqltypes.AutoString(length=2000),
131+
nullable=False,
132+
server_default="",
133+
),
134+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
135+
sa.ForeignKeyConstraint(["target_user_id"], ["user.id"]),
136+
sa.ForeignKeyConstraint(["performed_by_id"], ["user.id"]),
137+
sa.PrimaryKeyConstraint("id"),
138+
)
139+
140+
141+
def downgrade():
142+
op.drop_table("auditlog")
143+
op.drop_index(op.f("ix_companyinvite_token"), table_name="companyinvite")
144+
op.drop_table("companyinvite")
145+
op.drop_index(op.f("ix_company_cnpj"), table_name="company")
146+
op.drop_table("company")
147+
op.drop_table("item")
148+
op.drop_index(op.f("ix_user_email"), table_name="user")
149+
op.drop_table("user")

backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)