Skip to content

Commit fb548b1

Browse files
committed
A task to move from sqlite to postgres
1 parent 6e96b6b commit fb548b1

7 files changed

Lines changed: 261 additions & 29 deletions

File tree

src/alib/py/framework/jupiter/framework/appform/webapi/exceptions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Standard exceptions for the web API."""
22

33
from json import JSONDecodeError
4+
import logging
45

56
from jupiter.framework.appform.webapi.exception import (
67
WebApiError,

src/alib/py/framework/jupiter/framework/realm/standard.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,36 @@ def encode(self, value: _CompositeValueT) -> RealmThing:
820820
return result
821821

822822

823+
def _database_composite_json_maybe_parse(value: RealmThing) -> RealmThing:
824+
"""Normalize DB payloads for :class:`CompositeValue` decode.
825+
826+
VARCHAR / text columns and some SQLite→Postgres loaders expose JSON objects
827+
as a single ``str`` (sometimes nested JSON-as-string). Accept ``dict``,
828+
``Mapping``, or parseable JSON text; otherwise return ``value`` unchanged so
829+
callers can raise a precise error.
830+
"""
831+
node: RealmThing = value
832+
for _ in range(12):
833+
if isinstance(node, dict):
834+
return node
835+
if isinstance(node, Mapping):
836+
return dict(node)
837+
if isinstance(node, (bytes, bytearray)):
838+
node = node.decode()
839+
continue
840+
if isinstance(node, str):
841+
stripped = node.strip()
842+
if stripped == "" or stripped.lower() == "null":
843+
return node
844+
try:
845+
node = json.loads(node)
846+
except json.JSONDecodeError:
847+
return node
848+
continue
849+
break
850+
return node
851+
852+
823853
class _StandardCompositeValueDecoder(
824854
RealmDecoder[_CompositeValueT, _RealmT], Generic[_CompositeValueT, _RealmT]
825855
):
@@ -840,6 +870,8 @@ def __init__(
840870
self._realm = realm
841871

842872
def decode(self, value: RealmThing) -> _CompositeValueT:
873+
if self._realm is DatabaseRealm:
874+
value = _database_composite_json_maybe_parse(value)
843875
if not isinstance(value, dict):
844876
raise RealmDecodingError(
845877
f"Expected value for {self._the_type.__name__} to be a dict object"

src/core/jupiter/core/application/impl/postgres_repository.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from jupiter.core.big_plans.name import BigPlanName
2525
from jupiter.core.chores.name import ChoreName
2626
from jupiter.core.common.entity_icon import EntityIconDatabaseDecoder
27-
from jupiter.core.common.recurring_task_period import RecurringTaskPeriod
27+
from jupiter.core.common.recurring_task_gen_params import RecurringTaskGenParams
2828
from jupiter.core.common.sub.contacts.sub.contact.name import ContactName
2929
from jupiter.core.common.sub.inbox_tasks.name import InboxTaskName
3030
from jupiter.core.habits.name import HabitName
@@ -373,12 +373,7 @@ async def find_all_habit_summaries(
373373
) -> list[HabitSummary]:
374374
"""Find all summaries about habits."""
375375
query = """
376-
select
377-
ref_id,
378-
name,
379-
is_key,
380-
gen_params->>'period' as period,
381-
aspect_ref_id
376+
select ref_id, name, is_key, gen_params, aspect_ref_id
382377
from habit
383378
where habit_collection_ref_id = :parent_ref_id
384379
"""
@@ -399,8 +394,9 @@ async def find_all_habit_summaries(
399394
name=_HABIT_NAME_DECODER.decode(row["name"]),
400395
is_key=row["is_key"],
401396
period=self._realm_codec_registry.db_decode(
402-
RecurringTaskPeriod, row["period"]
403-
),
397+
RecurringTaskGenParams,
398+
row["gen_params"],
399+
).period,
404400
aspect_ref_id=_ENTITY_ID_DECODER.decode(str(row["aspect_ref_id"])),
405401
)
406402
for row in result

src/core/jupiter/core/search/use_case/search_index_backfill_do_all.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -338,13 +338,13 @@ async def _execute(
338338
entity_type,
339339
s.ref_id,
340340
)
341-
LOGGER.info(
342-
"Indexing %s:%s => %s time=%s",
343-
tag,
344-
s.ref_id,
345-
object_id,
346-
s.last_modified_time.value,
347-
)
341+
# LOGGER.info(
342+
# "Indexing %s:%s => %s time=%s",
343+
# tag,
344+
# s.ref_id,
345+
# object_id,
346+
# s.last_modified_time.value,
347+
# )
348348

349349
indexed_here += 1
350350

src/core/migrations/postgres/versions/2026_05_04_18_60_initial_reset.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,19 @@ def upgrade() -> None:
246246
big_plan_cnt INTEGER NOT NULL,
247247
created_time TIMESTAMP WITH TIME ZONE NOT NULL,
248248
last_modified_time TIMESTAMP WITH TIME ZONE NOT NULL,
249-
PRIMARY KEY (score_log_ref_id, period, timeline),
250249
FOREIGN KEY (score_log_ref_id) REFERENCES gamification_score_log (ref_id)
251250
)
252251
"""
253252
)
254253

254+
op.execute(
255+
"""
256+
CREATE UNIQUE INDEX uq_gamification_score_stats_natural_key
257+
ON gamification_score_stats (score_log_ref_id, period, timeline)
258+
NULLS NOT DISTINCT
259+
"""
260+
)
261+
255262
op.execute(
256263
"""
257264
CREATE INDEX ix_gamification_score_stats_score_log_ref_id_period_created_time
@@ -271,12 +278,24 @@ def upgrade() -> None:
271278
big_plan_cnt INTEGER NOT NULL,
272279
created_time TIMESTAMP WITH TIME ZONE NOT NULL,
273280
last_modified_time TIMESTAMP WITH TIME ZONE NOT NULL,
274-
PRIMARY KEY (score_log_ref_id, period, timeline, sub_period),
275281
FOREIGN KEY (score_log_ref_id) REFERENCES gamification_score_log (ref_id)
276282
)
277283
"""
278284
)
279285

286+
op.execute(
287+
"""
288+
CREATE UNIQUE INDEX uq_gamification_score_period_best_natural_key
289+
ON gamification_score_period_best (
290+
score_log_ref_id,
291+
period,
292+
timeline,
293+
sub_period
294+
)
295+
NULLS NOT DISTINCT
296+
"""
297+
)
298+
280299
# -------------------------------------------------------------------------
281300
# Vacation
282301
# -------------------------------------------------------------------------
@@ -526,7 +545,7 @@ def upgrade() -> None:
526545
archived_time TIMESTAMP WITH TIME ZONE,
527546
smart_list_collection_ref_id INTEGER,
528547
name VARCHAR(100) NOT NULL,
529-
icon VARCHAR(4),
548+
icon VARCHAR(64),
530549
archival_reason VARCHAR,
531550
CONSTRAINT pk_smart_list PRIMARY KEY (ref_id),
532551
CONSTRAINT fk_smart_list_smart_list_collection_ref_id_smart_list_collection
@@ -832,7 +851,7 @@ def upgrade() -> None:
832851
last_modified_time TIMESTAMP WITH TIME ZONE NOT NULL,
833852
archived_time TIMESTAMP WITH TIME ZONE,
834853
doc_collection_ref_id INTEGER NOT NULL,
835-
name VARCHAR(64) NOT NULL,
854+
name VARCHAR(255) NOT NULL,
836855
archival_reason VARCHAR,
837856
idempotency_key VARCHAR(36),
838857
parent_dir_ref_id INTEGER NOT NULL,
@@ -899,7 +918,7 @@ def upgrade() -> None:
899918
metric_collection_ref_id INTEGER,
900919
name VARCHAR NOT NULL,
901920
metric_unit VARCHAR,
902-
icon VARCHAR(4),
921+
icon VARCHAR(64),
903922
collection_params JSONB,
904923
archival_reason VARCHAR,
905924
is_key BOOLEAN NOT NULL,
@@ -1288,7 +1307,6 @@ def upgrade() -> None:
12881307
ON mutation_entity_event (entity_ref_id, name, timestamp, session_index)
12891308
"""
12901309
)
1291-
12921310
op.execute(
12931311
"""
12941312
CREATE INDEX ix_mutation_entity_event_entity_type_entity_ref_id_timestamp
@@ -2497,7 +2515,7 @@ def upgrade() -> None:
24972515
archived_time TIMESTAMP WITH TIME ZONE,
24982516
schedule_external_sync_log_ref_id INTEGER NOT NULL,
24992517
name VARCHAR(64) NOT NULL,
2500-
source VARCHAR(16) NOT NULL,
2518+
source VARCHAR(64) NOT NULL,
25012519
filter_schedule_stream_ref_id JSONB,
25022520
opened BOOLEAN NOT NULL,
25032521
per_stream_results JSONB NOT NULL,

src/core/migrations/sqlite/versions/2026_05_04_18_60_initial_reset.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Initial reset of the database.
22
3-
Revision ID: 6e5f8bc8fcd2
3+
Revision ID: 8d7c3e9f2a14
44
Revises: None
55
Create Date: 2026-05-04 18:60:00.000000
66
@@ -9,7 +9,7 @@
99
from alembic import op
1010

1111

12-
revision = "6e5f8bc8fcd2"
12+
revision = "8d7c3e9f2a14"
1313
down_revision = None
1414
branch_labels = None
1515
depends_on = None
@@ -526,7 +526,7 @@ def upgrade() -> None:
526526
archived_time DATETIME,
527527
smart_list_collection_ref_id INTEGER,
528528
name VARCHAR(100) NOT NULL,
529-
icon VARCHAR(4),
529+
icon VARCHAR(64),
530530
archival_reason VARCHAR,
531531
CONSTRAINT pk_smart_list PRIMARY KEY (ref_id),
532532
CONSTRAINT fk_smart_list_smart_list_collection_ref_id_smart_list_collection
@@ -832,7 +832,7 @@ def upgrade() -> None:
832832
last_modified_time DATETIME NOT NULL,
833833
archived_time DATETIME,
834834
doc_collection_ref_id INTEGER NOT NULL,
835-
name VARCHAR(64) NOT NULL,
835+
name VARCHAR(255) NOT NULL,
836836
archival_reason VARCHAR,
837837
idempotency_key VARCHAR(36),
838838
parent_dir_ref_id INTEGER NOT NULL,
@@ -899,7 +899,7 @@ def upgrade() -> None:
899899
metric_collection_ref_id INTEGER,
900900
name VARCHAR NOT NULL,
901901
metric_unit VARCHAR,
902-
icon VARCHAR(4),
902+
icon VARCHAR(64),
903903
collection_params JSON,
904904
archival_reason VARCHAR,
905905
is_key BOOLEAN NOT NULL,
@@ -2490,7 +2490,7 @@ def upgrade() -> None:
24902490
archived_time DATETIME,
24912491
schedule_external_sync_log_ref_id INTEGER NOT NULL,
24922492
name VARCHAR(64) NOT NULL,
2493-
source VARCHAR(16) NOT NULL,
2493+
source VARCHAR(64) NOT NULL,
24942494
filter_schedule_stream_ref_id JSON,
24952495
opened BOOLEAN NOT NULL,
24962496
per_stream_results JSON NOT NULL,

0 commit comments

Comments
 (0)