77 limit ,
88 offset ,
99 table ,
10- tsquery ,
1110 values ,
1211} from "decentraland-gatsby/dist/entities/Database/utils"
1312import { oneOf } from "decentraland-gatsby/dist/entities/Schema/utils"
@@ -20,13 +19,13 @@ import {
2019 WorldAttributes ,
2120 WorldListOrderBy ,
2221} from "./types"
23- import { DEFAULT_WORLD_IMAGE } from "../shared/constants"
2422import {
2523 buildTextsearch ,
2624 buildUpdateFavoritesQuery ,
2725 buildUpdateLikesQuery ,
2826 buildUserInteractionColumns ,
2927 buildUserInteractionJoins ,
28+ buildWorldTextSearchRank ,
3029} from "../shared/entityInteractions"
3130import UserFavoriteModel from "../UserFavorite/model"
3231import UserLikesModel from "../UserLikes/model"
@@ -47,7 +46,7 @@ export default class WorldModel extends Model<WorldAttributes> {
4746 static buildLatestPlaceLateralJoin ( worldAlias : string ) : SQLStatement {
4847 const a = SQL . raw ( worldAlias )
4948 return SQL `LEFT JOIN LATERAL (
50- SELECT p.contact_name, p.contact_email,
49+ SELECT p.image, p. contact_name, p.contact_email,
5150 p.creator_address, p.sdk, p.deployed_at
5251 FROM places p
5352 WHERE p.world_id = ${ a } .id AND p.disabled IS FALSE
@@ -98,9 +97,7 @@ export default class WorldModel extends Model<WorldAttributes> {
9897 ) }
9998 ${ conditional (
10099 ! ! options . search ,
101- SQL `AND ${ a } .textsearch @@ to_tsquery(${ tsquery (
102- options . search || ""
103- ) } )`
100+ SQL `AND ${ buildWorldTextSearchRank ( alias , options . search || "" ) } > 0`
104101 ) }
105102 ${ conditional (
106103 ( options . world_names ?. length ?? 0 ) > 0 ,
@@ -178,7 +175,7 @@ export default class WorldModel extends Model<WorldAttributes> {
178175 ) : SQLStatement {
179176 const forCount = opts ?. forCount ?? false
180177 const defaultSelectColumns = SQL `w.*
181- , COALESCE(w.image, ${ DEFAULT_WORLD_IMAGE } ) as image
178+ , COALESCE(w.image, lp.image ) as image
182179 , lp.contact_name
183180 , '0,0' as base_position
184181 , true as world
@@ -196,9 +193,7 @@ export default class WorldModel extends Model<WorldAttributes> {
196193 ) }
197194 ${ conditional (
198195 ! forCount && ! ! options . search ,
199- SQL `, ts_rank_cd(w.textsearch, to_tsquery(${ tsquery (
200- options . search || ""
201- ) } )) as rank`
196+ SQL `, ${ buildWorldTextSearchRank ( "w" , options . search || "" ) } as rank`
202197 ) }
203198 FROM ${ table ( this ) } w
204199 ${ this . buildLatestPlaceLateralJoin ( "w" ) }
@@ -257,7 +252,7 @@ export default class WorldModel extends Model<WorldAttributes> {
257252 ) : Promise < AggregateWorldAttributes | null > {
258253 const sql = SQL `
259254 SELECT w.*
260- , COALESCE(w.image, ${ DEFAULT_WORLD_IMAGE } ) as image
255+ , COALESCE(w.image, lp.image ) as image
261256 , lp.contact_name
262257 , '0,0' as base_position
263258 ${ conditional (
@@ -279,7 +274,13 @@ export default class WorldModel extends Model<WorldAttributes> {
279274 , 0 as user_visits
280275 , lp.deployed_at
281276 FROM ${ table ( this ) } w
282- ${ this . buildLatestPlaceLateralJoin ( "w" ) }
277+ LEFT JOIN LATERAL (
278+ SELECT p.image, p.contact_name, p.deployed_at
279+ FROM places p
280+ WHERE p.world_id = w.id AND p.disabled IS FALSE
281+ ORDER BY p.deployed_at DESC
282+ LIMIT 1
283+ ) lp ON true
283284 ${ conditional (
284285 ! ! options . user ,
285286 SQL `LEFT JOIN ${ table (
@@ -438,8 +439,6 @@ export default class WorldModel extends Model<WorldAttributes> {
438439 highlighted : world . highlighted ?? false ,
439440 highlighted_image : world . highlighted_image ?? null ,
440441 ranking : world . ranking ?? 0 ,
441- settings_configured : world . settings_configured ?? false ,
442- textsearch : undefined ,
443442 likes : world . likes ?? 0 ,
444443 dislikes : world . dislikes ?? 0 ,
445444 favorites : world . favorites ?? 0 ,
@@ -453,24 +452,22 @@ export default class WorldModel extends Model<WorldAttributes> {
453452 }
454453
455454 /**
456- * Insert a world or update its deployment-derived fields if settings haven 't been
457- * explicitly configured. Uses INSERT ... ON CONFLICT (id) DO UPDATE ... WHERE
458- * settings_configured = false so that user-configured worlds are never overwritten
459- * by deployments .
455+ * Insert a world only if it doesn 't already exist.
456+ * Uses INSERT ... ON CONFLICT (id) DO NOTHING for atomicity.
457+ * Returns the world ID (lowercased world_name) regardless of whether
458+ * the insert was performed .
460459 */
461460 static async insertWorldIfNotExists (
462461 world : Partial < WorldAttributes > & { world_name : string }
463462 ) : Promise < string > {
464463 const worldData = this . buildWorldData ( world )
465- const textsearch = this . textsearch ( worldData )
466464
467465 const sql = SQL `
468466 INSERT INTO ${ table ( this ) } (
469467 "id", "world_name", "title", "description", "image",
470468 "content_rating", "categories", "owner", "show_in_places",
471469 "single_player", "skybox_time", "is_private",
472470 "highlighted", "highlighted_image", "ranking",
473- "settings_configured", "textsearch",
474471 "likes", "dislikes", "favorites",
475472 "like_rate", "like_score", "disabled", "disabled_at",
476473 "created_at", "updated_at"
@@ -490,8 +487,6 @@ export default class WorldModel extends Model<WorldAttributes> {
490487 ${ worldData . highlighted } ,
491488 ${ worldData . highlighted_image } ,
492489 ${ worldData . ranking } ,
493- ${ worldData . settings_configured } ,
494- ${ textsearch } ,
495490 ${ worldData . likes } ,
496491 ${ worldData . dislikes } ,
497492 ${ worldData . favorites } ,
@@ -502,15 +497,7 @@ export default class WorldModel extends Model<WorldAttributes> {
502497 ${ worldData . created_at } ,
503498 ${ worldData . updated_at }
504499 )
505- ON CONFLICT (id) DO UPDATE SET
506- title = EXCLUDED.title,
507- description = EXCLUDED.description,
508- image = EXCLUDED.image,
509- content_rating = EXCLUDED.content_rating,
510- categories = EXCLUDED.categories,
511- textsearch = EXCLUDED.textsearch,
512- updated_at = EXCLUDED.updated_at
513- WHERE worlds.settings_configured = false
500+ ON CONFLICT (id) DO NOTHING
514501 `
515502
516503 await this . namedQuery ( "insert_world_if_not_exists" , sql )
@@ -520,15 +507,13 @@ export default class WorldModel extends Model<WorldAttributes> {
520507 static async upsertWorld (
521508 world : Partial < WorldAttributes > & { world_name : string }
522509 ) : Promise < WorldAttributes > {
523- const worldData = this . buildWorldData ( {
524- ...world ,
525- settings_configured : true ,
526- } )
527- const textsearch = this . textsearch ( worldData )
510+ const worldData = this . buildWorldData ( world )
528511
512+ // Fields that can be updated on conflict (excludes id, world_name, likes, etc.)
529513 const updatableFields : ( keyof WorldAttributes ) [ ] = [
530514 "title" ,
531515 "description" ,
516+ "image" ,
532517 "content_rating" ,
533518 "categories" ,
534519 "owner" ,
@@ -538,120 +523,22 @@ export default class WorldModel extends Model<WorldAttributes> {
538523 "is_private" ,
539524 ]
540525
541- const setClauses : SQLStatement [ ] = [
542- SQL `"settings_configured" = true` ,
543- SQL `"textsearch" = ${ textsearch } ` ,
544- SQL `"image" = EXCLUDED."image"` ,
545- SQL `"updated_at" = EXCLUDED."updated_at"` ,
546- ]
547-
526+ // Build changes object with only explicitly provided fields
527+ // This ensures we don't overwrite existing values with defaults on conflict
528+ const changes : Partial < WorldAttributes > = {
529+ updated_at : worldData . updated_at ,
530+ }
548531 for ( const field of updatableFields ) {
549532 if ( world [ field ] !== undefined ) {
550- setClauses . push ( SQL `" ${ SQL . raw ( field ) } " = EXCLUDED." ${ SQL . raw ( field ) } "` )
533+ ; ( changes as Record < string , unknown > ) [ field ] = world [ field ]
551534 }
552535 }
553536
554- const sql = SQL `
555- INSERT INTO ${ table ( this ) } (
556- "id", "world_name", "title", "description", "image",
557- "content_rating", "categories", "owner", "show_in_places",
558- "single_player", "skybox_time", "is_private",
559- "highlighted", "highlighted_image", "ranking",
560- "settings_configured", "textsearch",
561- "likes", "dislikes", "favorites",
562- "like_rate", "like_score", "disabled", "disabled_at",
563- "created_at", "updated_at"
564- ) VALUES (
565- ${ worldData . id } ,
566- ${ worldData . world_name } ,
567- ${ worldData . title } ,
568- ${ worldData . description } ,
569- ${ worldData . image } ,
570- ${ worldData . content_rating } ,
571- ${ worldData . categories } ,
572- ${ worldData . owner } ,
573- ${ worldData . show_in_places } ,
574- ${ worldData . single_player } ,
575- ${ worldData . skybox_time } ,
576- ${ worldData . is_private } ,
577- ${ worldData . highlighted } ,
578- ${ worldData . highlighted_image } ,
579- ${ worldData . ranking } ,
580- ${ worldData . settings_configured } ,
581- ${ textsearch } ,
582- ${ worldData . likes } ,
583- ${ worldData . dislikes } ,
584- ${ worldData . favorites } ,
585- ${ worldData . like_rate } ,
586- ${ worldData . like_score } ,
587- ${ worldData . disabled } ,
588- ${ worldData . disabled_at } ,
589- ${ worldData . created_at } ,
590- ${ worldData . updated_at }
591- )
592- ON CONFLICT (id) DO UPDATE SET
593- ${ join ( setClauses , SQL `, ` ) }
594- RETURNING *
595- `
596-
597- const results = await this . namedQuery < WorldAttributes > ( "upsert_world" , sql )
598- return results [ 0 ]
599- }
600-
601- /**
602- * Atomically delete place records for undeployed scenes and refresh the world's
603- * deployment-derived fields from the next-latest remaining scene. All operations
604- * happen in a single SQL statement (CTE-based) to prevent inconsistent state.
605- *
606- * If settings_configured = true, the world update is skipped (user settings preserved).
607- * If no remaining places exist, the UPDATE produces no rows (world stays as-is but
608- * won't appear in queries since they check EXISTS(places)).
609- */
610- static async deleteWorldScenesAndRefresh (
611- worldId : string ,
612- basePositions : string [ ] ,
613- eventTimestamp : number
614- ) : Promise < void > {
615- const normalizedWorldId = worldId . toLowerCase ( )
616- const eventDate = new Date ( eventTimestamp )
617-
618- const sql = SQL `
619- WITH deleted AS (
620- DELETE FROM places
621- WHERE world_id = ${ normalizedWorldId }
622- AND base_position = ANY(${ basePositions } )
623- AND deployed_at < ${ eventDate }
624- RETURNING world_id
625- ),
626- latest_remaining AS (
627- SELECT p.title, p.description, p.image, p.content_rating, p.categories
628- FROM places p
629- WHERE p.world_id = ${ normalizedWorldId }
630- AND p.disabled IS FALSE
631- AND p.base_position != ALL(${ basePositions } )
632- ORDER BY p.deployed_at DESC
633- LIMIT 1
634- )
635- UPDATE worlds SET
636- title = lr.title,
637- description = lr.description,
638- image = lr.image,
639- content_rating = COALESCE(lr.content_rating, worlds.content_rating),
640- categories = COALESCE(lr.categories, worlds.categories),
641- textsearch = (
642- setweight(to_tsvector(coalesce(lr.title, '')), 'A') ||
643- setweight(to_tsvector(coalesce(worlds.world_name, '')), 'A') ||
644- setweight(to_tsvector(coalesce(lr.description, '')), 'B') ||
645- setweight(to_tsvector(coalesce(worlds.owner, '')), 'C')
646- ),
647- updated_at = now()
648- FROM latest_remaining lr
649- WHERE worlds.id = ${ normalizedWorldId }
650- AND worlds.settings_configured = false
651- AND EXISTS (SELECT 1 FROM deleted)
652- `
653-
654- await this . namedQuery ( "delete_world_scenes_and_refresh" , sql )
537+ // Upsert on id (lowercased world_name) as the conflict target
538+ return this . upsert ( worldData , {
539+ target : [ "id" ] ,
540+ changes,
541+ } )
655542 }
656543
657544 static async disableWorld ( worldName : string ) : Promise < void > {
0 commit comments