Skip to content

Commit 0089c56

Browse files
wip: simplify upgrade
1 parent a101896 commit 0089c56

5 files changed

Lines changed: 51 additions & 276 deletions

File tree

fs_storage/migrations/19.0.1.1.2/post-migration.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@
1111
@openupgrade.migrate()
1212
def migrate(env, version):
1313
"""Install the glue module once fs_storage upgrade succeeded."""
14-
if not openupgrade.table_exists(env.cr, "fs_storage_env_snapshot"):
15-
_logger.info("No %s snapshot table found, skipping", "fs_storage_env_snapshot")
16-
return
17-
1814
module = env["ir.module.module"].search(
1915
[
2016
("name", "=", "fs_storage_environment"),
Lines changed: 51 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -1,224 +1,77 @@
11
# Copyright 2026 Camptocamp SA
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
33

4-
import json
5-
import logging
6-
74
from openupgradelib import openupgrade
8-
from psycopg2.extras import Json
9-
10-
_logger = logging.getLogger(__name__)
11-
12-
13-
def _get_fs_storage_server_env_fields():
14-
"""Return the exact fs.storage fields managed by server.env.mixin."""
15-
return [
16-
"protocol",
17-
"options",
18-
"directory_path",
19-
"eval_options_from_env",
20-
"model_xmlids",
21-
"field_xmlids",
22-
"check_connection_method",
23-
]
24-
25-
26-
def _get_sql_column_type(ttype):
27-
"""Map Odoo field types to temporary SQL column types."""
28-
if ttype in ("char", "selection"):
29-
return "varchar"
30-
if ttype in ("text", "html"):
31-
return "text"
32-
if ttype == "boolean":
33-
return "boolean"
34-
return None
35-
36-
37-
def _normalize_server_env_defaults(value):
38-
"""Normalize server_env_defaults to a Python dict."""
39-
if not value:
40-
return {}
41-
if isinstance(value, dict):
42-
return value
43-
if isinstance(value, str):
44-
return json.loads(value)
45-
return dict(value)
46-
47-
48-
def _cast_value_for_sql(value, ttype):
49-
"""Cast values before writing them into temporary physical columns."""
50-
if value is None:
51-
return None
52-
if ttype == "boolean":
53-
if isinstance(value, bool):
54-
return value
55-
if isinstance(value, str):
56-
return value.strip().lower() in ("1", "true", "yes", "on")
57-
return bool(value)
58-
return value
59-
60-
61-
@openupgrade.migrate()
62-
def migrate(env, version):
63-
"""Create temporary plain columns to migrate fs_storage data.
645

65-
During upgrade, fs_storage is loaded as a standard model before the glue
66-
module re-applies server.env.mixin behavior. We therefore:
67-
1. Snapshot the original schema.
68-
2. Snapshot server_env_defaults.
69-
3. Create temporary physical columns for the managed fields.
70-
4. Fill those columns from server_env_defaults.
71-
5. Force protocol to a valid value at the end.
72-
"""
73-
cr = env.cr
74-
env_fields = _get_fs_storage_server_env_fields()
6+
from odoo.tools import SQL
757

76-
if not openupgrade.table_exists(cr, "fs_storage"):
77-
_logger.info("Table fs_storage does not exist, skipping migration")
78-
return
8+
from odoo.addons.base.models.ir_model import field_xmlid
799

80-
# Snapshot the original physical schema so final cleanup can restore it.
81-
cr.execute(
82-
"""
83-
SELECT column_name
84-
FROM information_schema.columns
85-
WHERE table_schema = 'public'
86-
AND table_name = 'fs_storage'
87-
ORDER BY ordinal_position
88-
"""
89-
)
90-
original_columns = [row[0] for row in cr.fetchall()]
9110

92-
# Get metadata for the exact fs.storage server_env_defaults fields.
11+
def _get_server_env_mixin_fnames(cr, model: str) -> set[str]:
12+
possible_fnames = ("server_env_defaults", "tech_name")
9313
cr.execute(
94-
"""
95-
SELECT name, ttype, required
96-
FROM ir_model_fields
97-
WHERE model = 'fs.storage'
98-
AND name = ANY(%s)
99-
""",
100-
(env_fields,),
101-
)
102-
field_metadata = {
103-
name: {
104-
"ttype": ttype,
105-
"required": required,
106-
}
107-
for name, ttype, required in cr.fetchall()
108-
}
109-
managed_fields = [field for field in env_fields if field in field_metadata]
110-
111-
# Snapshot server_env_defaults for later restore and temporary value extraction.
112-
openupgrade.logged_query(
113-
cr,
114-
"""
115-
CREATE TABLE IF NOT EXISTS fs_storage_env_snapshot (
116-
id integer PRIMARY KEY,
117-
server_env_defaults jsonb,
118-
resolved_values jsonb NOT NULL
119-
)
120-
""",
121-
)
122-
openupgrade.logged_query(cr, "DELETE FROM fs_storage_env_snapshot")
123-
124-
# Snapshot the original schema so post-init cleanup can drop every extra column.
125-
openupgrade.logged_query(
126-
cr,
127-
"""
128-
CREATE TABLE IF NOT EXISTS fs_storage_env_original_columns (
129-
column_name varchar PRIMARY KEY
130-
)
131-
""",
132-
)
133-
openupgrade.logged_query(cr, "DELETE FROM fs_storage_env_original_columns")
134-
135-
for column_name in original_columns:
136-
cr.execute(
14+
SQL(
13715
"""
138-
INSERT INTO fs_storage_env_original_columns (column_name)
139-
VALUES (%s)
140-
ON CONFLICT (column_name) DO NOTHING
16+
SELECT name
17+
FROM ir_model_fields
18+
WHERE model = %(model)s
19+
AND name IN %(possible_fnames)s
14120
""",
142-
(column_name,),
21+
model=model,
22+
possible_fnames=possible_fnames,
14323
)
144-
145-
# Snapshot current server_env_defaults and prepare temporary resolved values.
146-
cr.execute(
147-
"""
148-
SELECT id, server_env_defaults
149-
FROM fs_storage
150-
ORDER BY id
151-
"""
15224
)
153-
rows = cr.fetchall()
154-
155-
for record_id, server_env_defaults in rows:
156-
server_env_defaults = _normalize_server_env_defaults(server_env_defaults)
157-
resolved_values = {}
25+
return set(row[0] for row in cr.fetchall())
15826

159-
for field_name in managed_fields:
160-
resolved_values[field_name] = server_env_defaults.get(
161-
f"x_{field_name}_env_default"
162-
)
16327

164-
cr.execute(
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
33+
cr.execute(
34+
SQL(
16535
"""
166-
INSERT INTO fs_storage_env_snapshot (
167-
id,
168-
server_env_defaults,
169-
resolved_values
170-
)
171-
VALUES (%s, %s, %s)
36+
SELECT name
37+
FROM ir_model_fields
38+
WHERE model = %(model)s
39+
AND name LIKE 'x_%%_env_default'
40+
OR name LIKE 'x_%%_env_is_editable'
17241
""",
173-
(
174-
record_id,
175-
Json(server_env_defaults),
176-
Json(resolved_values),
177-
),
42+
model=model,
17843
)
179-
180-
# Create only the temporary physical columns needed for the plain-model phase.
181-
for field_name in managed_fields:
182-
ttype = field_metadata[field_name]["ttype"]
183-
sql_type = _get_sql_column_type(ttype)
184-
if not sql_type:
185-
_logger.info("Skipping unsupported field %s of type %s", field_name, ttype)
186-
continue
187-
188-
if not openupgrade.column_exists(cr, "fs_storage", field_name):
189-
openupgrade.logged_query(
190-
cr,
191-
f'ALTER TABLE fs_storage ADD COLUMN "{field_name}" {sql_type}',
192-
)
193-
194-
# Fill temporary columns from the snapshot.
195-
cr.execute(
196-
"""
197-
SELECT id, resolved_values
198-
FROM fs_storage_env_snapshot
199-
ORDER BY id
200-
"""
20144
)
202-
for record_id, resolved_values in cr.fetchall():
203-
resolved_values = resolved_values or {}
204-
for field_name in managed_fields:
205-
ttype = field_metadata[field_name]["ttype"]
206-
sql_type = _get_sql_column_type(ttype)
207-
if not sql_type:
208-
continue
45+
return set(row[0] for row in cr.fetchall())
20946

210-
value = _cast_value_for_sql(resolved_values.get(field_name), ttype)
211-
cr.execute(
212-
f'UPDATE fs_storage SET "{field_name}" = %s WHERE id = %s',
213-
(value, record_id),
214-
)
21547

216-
# Ensure protocol is never NULL during the transient plain-model phase.
48+
def migrate(cr, version):
49+
if not version:
50+
return
51+
model = "fs.storage"
52+
mixin_fnames = _get_server_env_mixin_fnames(cr, model)
53+
magic_fnames = _get_server_env_mixin_magic_fnames(cr, model)
54+
to_move_fnames = mixin_fnames | magic_fnames
55+
old_module = "fs_storage"
56+
new_module = "fs_storage_environment"
57+
rename_specs = [
58+
(field_xmlid(old_module, model, fname), field_xmlid(new_module, model, fname))
59+
for fname in to_move_fnames
60+
]
61+
openupgrade.rename_xmlids(cr, rename_specs, allow_merge=True)
62+
# Add noupdate to the magic_fnames, to prevent Odoo from deleting them in upgrade
21763
openupgrade.logged_query(
21864
cr,
21965
"""
220-
UPDATE fs_storage
221-
SET protocol = 'odoofs'
222-
WHERE protocol IS NULL
66+
UPDATE ir_model_data SET noupdate = TRUE
67+
WHERE module = %(module)s
68+
AND name IN %(names)s
22369
""",
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+
),
22477
)

fs_storage_environment/__init__.py

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

fs_storage_environment/__manifest__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
"author": " ACSONE SA/NV, Odoo Community Association (OCA)",
1212
"license": "LGPL-3",
1313
"development_status": "Beta",
14-
"installable": True,
1514
"depends": ["fs_storage", "server_environment"],
1615
"auto_install": True,
17-
"post_init_hook": "post_init_hook",
1816
}

fs_storage_environment/hooks.py

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

0 commit comments

Comments
 (0)