Skip to content

Commit 64550e6

Browse files
committed
fixup! wip: simplify upgrade
1 parent cf72d6f commit 64550e6

4 files changed

Lines changed: 143 additions & 22 deletions

File tree

fs_storage/migrations/19.0.1.1.2/pre-migration.py

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,69 +9,127 @@
99

1010

1111
def _get_server_env_mixin_fnames(cr, model: str) -> set[str]:
12+
"""Return the core mixin field names present on the model."""
1213
possible_fnames = ("server_env_defaults", "tech_name")
1314
cr.execute(
1415
SQL(
1516
"""
1617
SELECT name
1718
FROM ir_model_fields
1819
WHERE model = %(model)s
19-
AND name IN %(possible_fnames)s
20+
AND name IN %(possible_fnames)s
2021
""",
2122
model=model,
2223
possible_fnames=possible_fnames,
2324
)
2425
)
25-
return set(row[0] for row in cr.fetchall())
26+
return {row[0] for row in cr.fetchall()}
2627

2728

28-
def _get_server_env_mixin_magic_fnames(cr, model: str) -> set[str]:
29-
"""Find all the magic server environment field names for a given model"""
30-
# Get all fields created on-the-fly by the mixin. Following the name patterns:
31-
# - default fields: x_<field_name>_env_default
32-
# - is editable fields: x_<field_name>_env_is_editable
29+
def _get_server_env_mixin_magic_fnames(cr, model):
30+
"""Return the dynamic server_environment helper fields for the model.
31+
32+
These are the fields created on the fly by server.env.mixin:
33+
- x_<field_name>_env_default
34+
- x_<field_name>_env_is_editable
35+
"""
3336
cr.execute(
3437
SQL(
3538
"""
3639
SELECT name
3740
FROM ir_model_fields
3841
WHERE model = %(model)s
39-
AND name LIKE 'x_%%_env_default'
40-
OR name LIKE 'x_%%_env_is_editable'
42+
AND (
43+
name LIKE 'x_%%_env_default'
44+
OR name LIKE 'x_%%_env_is_editable'
45+
)
4146
""",
4247
model=model,
4348
)
4449
)
45-
return set(row[0] for row in cr.fetchall())
50+
return {row[0] for row in cr.fetchall()}
51+
52+
53+
def _get_existing_columns(cr, table_name):
54+
"""Return the current physical columns of a PostgreSQL table."""
55+
cr.execute(
56+
SQL(
57+
"""
58+
SELECT column_name
59+
FROM information_schema.columns
60+
WHERE table_schema = 'public'
61+
AND table_name = %(table_name)s
62+
ORDER BY ordinal_position
63+
""",
64+
table_name=table_name,
65+
)
66+
)
67+
return [row[0] for row in cr.fetchall()]
4668

4769

4870
def migrate(cr, version):
71+
"""Move XMLIDs to the glue module and snapshot the original schema.
72+
73+
This keeps the server_environment ir.model.fields metadata attached to the
74+
new module and preserves field values.
75+
In the end, the original fs_storage schema is preserved at post cleanup.
76+
"""
77+
4978
if not version:
5079
return
80+
5181
model = "fs.storage"
82+
old_module = "fs_storage"
83+
new_module = "fs_storage_environment"
84+
5285
mixin_fnames = _get_server_env_mixin_fnames(cr, model)
5386
magic_fnames = _get_server_env_mixin_magic_fnames(cr, model)
5487
to_move_fnames = mixin_fnames | magic_fnames
55-
old_module = "fs_storage"
56-
new_module = "fs_storage_environment"
88+
5789
rename_specs = [
5890
(field_xmlid(old_module, model, fname), field_xmlid(new_module, model, fname))
5991
for fname in to_move_fnames
6092
]
6193
openupgrade.rename_xmlids(cr, rename_specs, allow_merge=True)
94+
6295
# Add noupdate to the magic_fnames, to prevent Odoo from deleting them in upgrade
96+
if magic_fnames:
97+
openupgrade.logged_query(
98+
cr,
99+
"""
100+
UPDATE ir_model_data SET noupdate = TRUE
101+
WHERE module = %(module)s
102+
AND name IN %(names)s
103+
""",
104+
dict(
105+
module=new_module,
106+
names=tuple(
107+
field_xmlid(new_module, model, fname).split(".")[1]
108+
for fname in magic_fnames
109+
),
110+
),
111+
)
112+
113+
# Snapshot the original physical schema.
114+
# Post cleanup hook in `fs_storage_environment` will compare
115+
# the final table with this snapshot and drop every created extra column.
116+
original_columns = _get_existing_columns(cr, "fs_storage")
63117
openupgrade.logged_query(
64118
cr,
65119
"""
66-
UPDATE ir_model_data SET noupdate = TRUE
67-
WHERE module = %(module)s
68-
AND name IN %(names)s
120+
CREATE TABLE IF NOT EXISTS fs_storage_original_columns (
121+
column_name varchar PRIMARY KEY
122+
)
69123
""",
70-
dict(
71-
module=new_module,
72-
names=tuple(
73-
field_xmlid(new_module, model, fname).split(".")[1]
74-
for fname in magic_fnames
75-
),
76-
),
77124
)
125+
openupgrade.logged_query(cr, "DELETE FROM fs_storage_original_columns")
126+
127+
for column_name in original_columns:
128+
cr.execute(
129+
"""
130+
INSERT INTO fs_storage_original_columns (column_name)
131+
VALUES (%s)
132+
ON CONFLICT (column_name) DO NOTHING
133+
""",
134+
(column_name,),
135+
)

fs_storage_environment/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from . import models
2+
from .hooks import post_init_hook

fs_storage_environment/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@
1313
"development_status": "Beta",
1414
"depends": ["fs_storage", "server_environment"],
1515
"auto_install": True,
16+
"post_init_hook": "post_init_hook",
1617
}

fs_storage_environment/hooks.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
3+
4+
import logging
5+
6+
from openupgradelib import openupgrade
7+
8+
_logger = logging.getLogger(__name__)
9+
10+
# This post_init_hook is needed as a part of migration for those environments
11+
# which had originally `fs_storage` installed with `server_environment` dependency.
12+
# When migration is performed, `fs_storage` is being upgraded
13+
# and `server.env.mixin` is not yet applied, the server env fields
14+
# coming from `fs.storage` and the inheriting models are created as regular fields,
15+
# so new database columns are created for them, which we don't want.
16+
17+
18+
def _get_table_columns(cr, table_name):
19+
"""Return the current physical columns of a PostgreSQL table."""
20+
cr.execute(
21+
"""
22+
SELECT column_name
23+
FROM information_schema.columns
24+
WHERE table_schema = 'public'
25+
AND table_name = %s
26+
ORDER BY ordinal_position
27+
""",
28+
(table_name,),
29+
)
30+
return [row[0] for row in cr.fetchall()]
31+
32+
33+
def post_init_hook(env):
34+
"""Restore the original fs_storage schema by column diff."""
35+
cr = env.cr
36+
37+
if not openupgrade.table_exists(cr, "fs_storage_original_columns"):
38+
_logger.info(
39+
"""
40+
Original columns snapshot table not found.
41+
Skipping fs_storage schema restoration as it is likely not needed.
42+
"""
43+
)
44+
return
45+
46+
cr.execute("SELECT column_name FROM fs_storage_original_columns")
47+
original_columns = {row[0] for row in cr.fetchall()}
48+
current_columns = set(_get_table_columns(cr, "fs_storage"))
49+
columns_to_drop = sorted(current_columns - original_columns)
50+
51+
for column_name in columns_to_drop:
52+
if openupgrade.column_exists(cr, "fs_storage", column_name):
53+
openupgrade.logged_query(
54+
cr,
55+
f'ALTER TABLE fs_storage DROP COLUMN "{column_name}"',
56+
)
57+
58+
_logger.info("Dropped extra fs_storage columns: %s", columns_to_drop)
59+
60+
if openupgrade.table_exists(cr, "fs_storage_original_columns"):
61+
openupgrade.logged_query(cr, "DROP TABLE fs_storage_original_columns")

0 commit comments

Comments
 (0)