From 78a35e5d1fccc4948b6ae6ee8357db6fe8ad57d9 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 5 Mar 2026 02:30:12 +0000 Subject: [PATCH] Make parcels attached to addresses optional --- db/dbinfo/site.bob.go | 4 +- db/factory/bobfactory_main.bob.go | 2 +- db/factory/parcel.bob.go | 2 +- db/factory/site.bob.go | 79 ++++++++++++-------- db/migrations/00087_site_parcel_nullable.sql | 4 + db/models/parcel.bob.go | 9 ++- db/models/site.bob.go | 29 +++---- platform/csv/csv.go | 6 +- 8 files changed, 84 insertions(+), 51 deletions(-) create mode 100644 db/migrations/00087_site_parcel_nullable.sql diff --git a/db/dbinfo/site.bob.go b/db/dbinfo/site.bob.go index f72d5827..a40b5484 100644 --- a/db/dbinfo/site.bob.go +++ b/db/dbinfo/site.bob.go @@ -99,9 +99,9 @@ var Sites = Table[ ParcelID: column{ Name: "parcel_id", DBType: "integer", - Default: "", + Default: "NULL", Comment: "", - Nullable: false, + Nullable: true, Generated: false, AutoIncr: false, }, diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index 1fcca391..4149de60 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -4483,7 +4483,7 @@ func (f *Factory) FromExistingSite(m *models.Site) *SiteTemplate { o.OrganizationID = func() int32 { return m.OrganizationID } o.OwnerName = func() string { return m.OwnerName } o.OwnerPhoneE164 = func() null.Val[string] { return m.OwnerPhoneE164 } - o.ParcelID = func() int32 { return m.ParcelID } + o.ParcelID = func() null.Val[int32] { return m.ParcelID } o.ResidentOwned = func() null.Val[bool] { return m.ResidentOwned } o.Tags = func() pgtypes.HStore { return m.Tags } o.Version = func() int32 { return m.Version } diff --git a/db/factory/parcel.bob.go b/db/factory/parcel.bob.go index eb3bee7a..03019eea 100644 --- a/db/factory/parcel.bob.go +++ b/db/factory/parcel.bob.go @@ -71,7 +71,7 @@ func (t ParcelTemplate) setModelRels(o *models.Parcel) { for _, r := range t.r.Sites { related := r.o.BuildMany(r.number) for _, rel := range related { - rel.ParcelID = o.ID // h2 + rel.ParcelID = null.From(o.ID) // h2 rel.R.Parcel = o } rel = append(rel, related...) diff --git a/db/factory/site.bob.go b/db/factory/site.bob.go index ada316d4..7f62bbb5 100644 --- a/db/factory/site.bob.go +++ b/db/factory/site.bob.go @@ -47,7 +47,7 @@ type SiteTemplate struct { OrganizationID func() int32 OwnerName func() string OwnerPhoneE164 func() null.Val[string] - ParcelID func() int32 + ParcelID func() null.Val[int32] ResidentOwned func() null.Val[bool] Tags func() pgtypes.HStore Version func() int32 @@ -169,7 +169,7 @@ func (t SiteTemplate) setModelRels(o *models.Site) { if t.r.Parcel != nil { rel := t.r.Parcel.o.Build() rel.R.Sites = append(rel.R.Sites, o) - o.ParcelID = rel.ID // h2 + o.ParcelID = null.From(rel.ID) // h2 o.R.Parcel = rel } } @@ -217,7 +217,7 @@ func (o SiteTemplate) BuildSetter() *models.SiteSetter { } if o.ParcelID != nil { val := o.ParcelID() - m.ParcelID = omit.From(val) + m.ParcelID = omitnull.FromNull(val) } if o.ResidentOwned != nil { val := o.ResidentOwned() @@ -336,10 +336,6 @@ func ensureCreatableSite(m *models.SiteSetter) { val := random_string(nil) m.OwnerName = omit.From(val) } - if !(m.ParcelID.IsValue()) { - val := random_int32(nil) - m.ParcelID = omit.From(val) - } if !(m.Tags.IsValue()) { val := random_pgtypes_HStore(nil) m.Tags = omit.From(val) @@ -435,6 +431,25 @@ func (o *SiteTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m * } + isParcelDone, _ := siteRelParcelCtx.Value(ctx) + if !isParcelDone && o.r.Parcel != nil { + ctx = siteRelParcelCtx.WithValue(ctx, true) + if o.r.Parcel.o.alreadyPersisted { + m.R.Parcel = o.r.Parcel.o.Build() + } else { + var rel6 *models.Parcel + rel6, err = o.r.Parcel.o.Create(ctx, exec) + if err != nil { + return err + } + err = m.AttachParcel(ctx, exec, rel6) + if err != nil { + return err + } + } + + } + return err } @@ -479,23 +494,6 @@ func (o *SiteTemplate) Create(ctx context.Context, exec bob.Executor) (*models.S opt.CreatorID = omit.From(rel4.ID) - if o.r.Parcel == nil { - SiteMods.WithNewParcel().Apply(ctx, o) - } - - var rel6 *models.Parcel - - if o.r.Parcel.o.alreadyPersisted { - rel6 = o.r.Parcel.o.Build() - } else { - rel6, err = o.r.Parcel.o.Create(ctx, exec) - if err != nil { - return nil, err - } - } - - opt.ParcelID = omit.From(rel6.ID) - m, err := models.Sites.Insert(opt).One(ctx, exec) if err != nil { return nil, err @@ -503,7 +501,6 @@ func (o *SiteTemplate) Create(ctx context.Context, exec bob.Executor) (*models.S m.R.Address = rel3 m.R.CreatorUser = rel4 - m.R.Parcel = rel6 if err := o.insertOptRels(ctx, exec, m); err != nil { return nil, err @@ -922,14 +919,14 @@ func (m siteMods) RandomOwnerPhoneE164NotNull(f *faker.Faker) SiteMod { } // Set the model columns to this value -func (m siteMods) ParcelID(val int32) SiteMod { +func (m siteMods) ParcelID(val null.Val[int32]) SiteMod { return SiteModFunc(func(_ context.Context, o *SiteTemplate) { - o.ParcelID = func() int32 { return val } + o.ParcelID = func() null.Val[int32] { return val } }) } // Set the Column from the function -func (m siteMods) ParcelIDFunc(f func() int32) SiteMod { +func (m siteMods) ParcelIDFunc(f func() null.Val[int32]) SiteMod { return SiteModFunc(func(_ context.Context, o *SiteTemplate) { o.ParcelID = f }) @@ -944,10 +941,32 @@ func (m siteMods) UnsetParcelID() SiteMod { // Generates a random value for the column using the given faker // if faker is nil, a default faker is used +// The generated value is sometimes null func (m siteMods) RandomParcelID(f *faker.Faker) SiteMod { return SiteModFunc(func(_ context.Context, o *SiteTemplate) { - o.ParcelID = func() int32 { - return random_int32(f) + o.ParcelID = func() null.Val[int32] { + if f == nil { + f = &defaultFaker + } + + val := random_int32(f) + return null.From(val) + } + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +// The generated value is never null +func (m siteMods) RandomParcelIDNotNull(f *faker.Faker) SiteMod { + return SiteModFunc(func(_ context.Context, o *SiteTemplate) { + o.ParcelID = func() null.Val[int32] { + if f == nil { + f = &defaultFaker + } + + val := random_int32(f) + return null.From(val) } }) } diff --git a/db/migrations/00087_site_parcel_nullable.sql b/db/migrations/00087_site_parcel_nullable.sql new file mode 100644 index 00000000..0f11cc06 --- /dev/null +++ b/db/migrations/00087_site_parcel_nullable.sql @@ -0,0 +1,4 @@ +-- +goose Up +ALTER TABLE site ALTER COLUMN parcel_id DROP NOT NULL; +-- +goose Down +ALTER TABLE site ALTER COLUMN parcel_id ADD NOT NULL; diff --git a/db/models/parcel.bob.go b/db/models/parcel.bob.go index dc1d0c97..d39dc09a 100644 --- a/db/models/parcel.bob.go +++ b/db/models/parcel.bob.go @@ -444,7 +444,7 @@ func (os ParcelSlice) Sites(mods ...bob.Mod[*dialect.SelectQuery]) SitesQuery { func insertParcelSites0(ctx context.Context, exec bob.Executor, sites1 []*SiteSetter, parcel0 *Parcel) (SiteSlice, error) { for i := range sites1 { - sites1[i].ParcelID = omit.From(parcel0.ID) + sites1[i].ParcelID = omitnull.From(parcel0.ID) } ret, err := Sites.Insert(bob.ToMods(sites1...)).All(ctx, exec) @@ -457,7 +457,7 @@ func insertParcelSites0(ctx context.Context, exec bob.Executor, sites1 []*SiteSe func attachParcelSites0(ctx context.Context, exec bob.Executor, count int, sites1 SiteSlice, parcel0 *Parcel) (SiteSlice, error) { setter := &SiteSetter{ - ParcelID: omit.From(parcel0.ID), + ParcelID: omitnull.From(parcel0.ID), } err := sites1.UpdateAll(ctx, exec, *setter) @@ -628,7 +628,10 @@ func (os ParcelSlice) LoadSites(ctx context.Context, exec bob.Executor, mods ... for _, rel := range sites { - if !(o.ID == rel.ParcelID) { + if !rel.ParcelID.IsValue() { + continue + } + if !(rel.ParcelID.IsValue() && o.ID == rel.ParcelID.MustGet()) { continue } diff --git a/db/models/site.bob.go b/db/models/site.bob.go index 8d2b5ce7..3527d03f 100644 --- a/db/models/site.bob.go +++ b/db/models/site.bob.go @@ -35,7 +35,7 @@ type Site struct { OrganizationID int32 `db:"organization_id" ` OwnerName string `db:"owner_name" ` OwnerPhoneE164 null.Val[string] `db:"owner_phone_e164" ` - ParcelID int32 `db:"parcel_id" ` + ParcelID null.Val[int32] `db:"parcel_id" ` ResidentOwned null.Val[bool] `db:"resident_owned" ` Tags pgtypes.HStore `db:"tags" ` Version int32 `db:"version,pk" ` @@ -127,7 +127,7 @@ type SiteSetter struct { OrganizationID omit.Val[int32] `db:"organization_id" ` OwnerName omit.Val[string] `db:"owner_name" ` OwnerPhoneE164 omitnull.Val[string] `db:"owner_phone_e164" ` - ParcelID omit.Val[int32] `db:"parcel_id" ` + ParcelID omitnull.Val[int32] `db:"parcel_id" ` ResidentOwned omitnull.Val[bool] `db:"resident_owned" ` Tags omit.Val[pgtypes.HStore] `db:"tags" ` Version omit.Val[int32] `db:"version,pk" ` @@ -162,7 +162,7 @@ func (s SiteSetter) SetColumns() []string { if !s.OwnerPhoneE164.IsUnset() { vals = append(vals, "owner_phone_e164") } - if s.ParcelID.IsValue() { + if !s.ParcelID.IsUnset() { vals = append(vals, "parcel_id") } if !s.ResidentOwned.IsUnset() { @@ -205,8 +205,8 @@ func (s SiteSetter) Overwrite(t *Site) { if !s.OwnerPhoneE164.IsUnset() { t.OwnerPhoneE164 = s.OwnerPhoneE164.MustGetNull() } - if s.ParcelID.IsValue() { - t.ParcelID = s.ParcelID.MustGet() + if !s.ParcelID.IsUnset() { + t.ParcelID = s.ParcelID.MustGetNull() } if !s.ResidentOwned.IsUnset() { t.ResidentOwned = s.ResidentOwned.MustGetNull() @@ -280,8 +280,8 @@ func (s *SiteSetter) Apply(q *dialect.InsertQuery) { vals[8] = psql.Raw("DEFAULT") } - if s.ParcelID.IsValue() { - vals[9] = psql.Arg(s.ParcelID.MustGet()) + if !s.ParcelID.IsUnset() { + vals[9] = psql.Arg(s.ParcelID.MustGetNull()) } else { vals[9] = psql.Raw("DEFAULT") } @@ -378,7 +378,7 @@ func (s SiteSetter) Expressions(prefix ...string) []bob.Expression { }}) } - if s.ParcelID.IsValue() { + if !s.ParcelID.IsUnset() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "parcel_id")...), psql.Arg(s.ParcelID), @@ -806,7 +806,7 @@ func (o *Site) Parcel(mods ...bob.Mod[*dialect.SelectQuery]) ParcelsQuery { } func (os SiteSlice) Parcel(mods ...bob.Mod[*dialect.SelectQuery]) ParcelsQuery { - pkParcelID := make(pgtypes.Array[int32], 0, len(os)) + pkParcelID := make(pgtypes.Array[null.Val[int32]], 0, len(os)) for _, o := range os { if o == nil { continue @@ -1178,7 +1178,7 @@ func (site0 *Site) AttachFile(ctx context.Context, exec bob.Executor, fileupload func attachSiteParcel0(ctx context.Context, exec bob.Executor, count int, site0 *Site, parcel1 *Parcel) (*Site, error) { setter := &SiteSetter{ - ParcelID: omit.From(parcel1.ID), + ParcelID: omitnull.From(parcel1.ID), } err := site0.Update(ctx, exec, setter) @@ -1234,7 +1234,7 @@ type siteWhere[Q psql.Filterable] struct { OrganizationID psql.WhereMod[Q, int32] OwnerName psql.WhereMod[Q, string] OwnerPhoneE164 psql.WhereNullMod[Q, string] - ParcelID psql.WhereMod[Q, int32] + ParcelID psql.WhereNullMod[Q, int32] ResidentOwned psql.WhereNullMod[Q, bool] Tags psql.WhereMod[Q, pgtypes.HStore] Version psql.WhereMod[Q, int32] @@ -1255,7 +1255,7 @@ func buildSiteWhere[Q psql.Filterable](cols siteColumns) siteWhere[Q] { OrganizationID: psql.Where[Q, int32](cols.OrganizationID), OwnerName: psql.Where[Q, string](cols.OwnerName), OwnerPhoneE164: psql.WhereNull[Q, string](cols.OwnerPhoneE164), - ParcelID: psql.Where[Q, int32](cols.ParcelID), + ParcelID: psql.WhereNull[Q, int32](cols.ParcelID), ResidentOwned: psql.WhereNull[Q, bool](cols.ResidentOwned), Tags: psql.Where[Q, pgtypes.HStore](cols.Tags), Version: psql.Where[Q, int32](cols.Version), @@ -1903,8 +1903,11 @@ func (os SiteSlice) LoadParcel(ctx context.Context, exec bob.Executor, mods ...b } for _, rel := range parcels { + if !o.ParcelID.IsValue() { + continue + } - if !(o.ParcelID == rel.ID) { + if !(o.ParcelID.IsValue() && o.ParcelID.MustGet() == rel.ID) { continue } diff --git a/platform/csv/csv.go b/platform/csv/csv.go index 0f0d1d79..17751a27 100644 --- a/platform/csv/csv.go +++ b/platform/csv/csv.go @@ -79,6 +79,10 @@ func JobCommit(ctx context.Context, file_id int32) error { if err.Error() != "sql: no rows in result set" { return fmt.Errorf("query site: %w", err) } + var parcel_id *int32 + if parcel != nil { + parcel_id = &(*parcel).ID + } setter := models.SiteSetter{ AddressID: omit.From(address.ID), Created: omit.From(time.Now()), @@ -89,7 +93,7 @@ func JobCommit(ctx context.Context, file_id int32) error { OrganizationID: omit.From(org.ID), OwnerName: omit.From(row.PropertyOwnerName), OwnerPhoneE164: omitnull.FromPtr(row.PropertyOwnerPhoneE164.Ptr()), - ParcelID: omit.From(parcel.ID), + ParcelID: omitnull.FromPtr(parcel_id), ResidentOwned: omitnull.FromPtr(row.ResidentOwned.Ptr()), Tags: omit.From(row.Tags), Version: omit.From(int32(1)),