diff --git a/db/dbinfo/fileupload.pool.bob.go b/db/dbinfo/fileupload.pool.bob.go index 55291872..957485be 100644 --- a/db/dbinfo/fileupload.pool.bob.go +++ b/db/dbinfo/fileupload.pool.bob.go @@ -222,6 +222,15 @@ var FileuploadPools = Table[ Generated: false, AutoIncr: false, }, + AddressID: column{ + Name: "address_id", + DBType: "integer", + Default: "NULL", + Comment: "", + Nullable: true, + Generated: false, + AutoIncr: false, + }, }, Indexes: fileuploadPoolIndexes{ PoolPkey: index{ @@ -248,6 +257,15 @@ var FileuploadPools = Table[ Comment: "", }, ForeignKeys: fileuploadPoolForeignKeys{ + FileuploadPoolPoolAddressIDFkey: foreignKey{ + constraint: constraint{ + Name: "fileupload.pool.pool_address_id_fkey", + Columns: []string{"address_id"}, + Comment: "", + }, + ForeignTable: "address", + ForeignColumns: []string{"id"}, + }, FileuploadPoolPoolCreatorIDFkey: foreignKey{ constraint: constraint{ Name: "fileupload.pool.pool_creator_id_fkey", @@ -313,11 +331,12 @@ type fileuploadPoolColumns struct { AddressLocality column AddressRegion column Condition column + AddressID column } func (c fileuploadPoolColumns) AsSlice() []column { return []column{ - c.AddressPostalCode, c.AddressStreet, c.Committed, c.Created, c.CreatorID, c.CSVFile, c.Deleted, c.Geom, c.H3cell, c.ID, c.IsInDistrict, c.IsNew, c.Notes, c.PropertyOwnerName, c.PropertyOwnerPhoneE164, c.ResidentOwned, c.ResidentPhoneE164, c.LineNumber, c.Tags, c.AddressNumber, c.AddressLocality, c.AddressRegion, c.Condition, + c.AddressPostalCode, c.AddressStreet, c.Committed, c.Created, c.CreatorID, c.CSVFile, c.Deleted, c.Geom, c.H3cell, c.ID, c.IsInDistrict, c.IsNew, c.Notes, c.PropertyOwnerName, c.PropertyOwnerPhoneE164, c.ResidentOwned, c.ResidentPhoneE164, c.LineNumber, c.Tags, c.AddressNumber, c.AddressLocality, c.AddressRegion, c.Condition, c.AddressID, } } @@ -332,6 +351,7 @@ func (i fileuploadPoolIndexes) AsSlice() []index { } type fileuploadPoolForeignKeys struct { + FileuploadPoolPoolAddressIDFkey foreignKey FileuploadPoolPoolCreatorIDFkey foreignKey FileuploadPoolPoolCSVFileFkey foreignKey FileuploadPoolPoolPropertyOwnerPhoneE164Fkey foreignKey @@ -340,7 +360,7 @@ type fileuploadPoolForeignKeys struct { func (f fileuploadPoolForeignKeys) AsSlice() []foreignKey { return []foreignKey{ - f.FileuploadPoolPoolCreatorIDFkey, f.FileuploadPoolPoolCSVFileFkey, f.FileuploadPoolPoolPropertyOwnerPhoneE164Fkey, f.FileuploadPoolPoolResidentPhoneE164Fkey, + f.FileuploadPoolPoolAddressIDFkey, f.FileuploadPoolPoolCreatorIDFkey, f.FileuploadPoolPoolCSVFileFkey, f.FileuploadPoolPoolPropertyOwnerPhoneE164Fkey, f.FileuploadPoolPoolResidentPhoneE164Fkey, } } diff --git a/db/migrations/00136_fileupload_pool_address.sql b/db/migrations/00136_fileupload_pool_address.sql new file mode 100644 index 00000000..feb8f86c --- /dev/null +++ b/db/migrations/00136_fileupload_pool_address.sql @@ -0,0 +1,4 @@ +-- +goose Up +ALTER TABLE fileupload.pool ADD COLUMN address_id INTEGER REFERENCES address(id); +-- +goose Down +ALTER TABLE fileupload.pool DROP COLUMN address_id; diff --git a/db/models/address.bob.go b/db/models/address.bob.go index 2e1ed705..0e9c072c 100644 --- a/db/models/address.bob.go +++ b/db/models/address.bob.go @@ -56,6 +56,7 @@ type AddressesQuery = *psql.ViewQuery[*Address, AddressSlice] // addressR is where relationships are stored. type addressR struct { Mailers CommsMailerSlice // comms.mailer.mailer_address_id_fkey + Pools FileuploadPoolSlice // fileupload.pool.pool_address_id_fkey NuisanceOlds PublicreportNuisanceOldSlice // publicreport.nuisance_old.nuisance_address_id_fkey Reports PublicreportReportSlice // publicreport.report.report_address_id_fkey WaterOlds PublicreportWaterOldSlice // publicreport.water_old.pool_address_id_fkey @@ -635,6 +636,30 @@ func (os AddressSlice) Mailers(mods ...bob.Mod[*dialect.SelectQuery]) CommsMaile )...) } +// Pools starts a query for related objects on fileupload.pool +func (o *Address) Pools(mods ...bob.Mod[*dialect.SelectQuery]) FileuploadPoolsQuery { + return FileuploadPools.Query(append(mods, + sm.Where(FileuploadPools.Columns.AddressID.EQ(psql.Arg(o.ID))), + )...) +} + +func (os AddressSlice) Pools(mods ...bob.Mod[*dialect.SelectQuery]) FileuploadPoolsQuery { + pkID := make(pgtypes.Array[int32], 0, len(os)) + for _, o := range os { + if o == nil { + continue + } + pkID = append(pkID, o.ID) + } + PKArgExpr := psql.Select(sm.Columns( + psql.F("unnest", psql.Cast(psql.Arg(pkID), "integer[]")), + )) + + return FileuploadPools.Query(append(mods, + sm.Where(psql.Group(FileuploadPools.Columns.AddressID).OP("IN", PKArgExpr)), + )...) +} + // NuisanceOlds starts a query for related objects on publicreport.nuisance_old func (o *Address) NuisanceOlds(mods ...bob.Mod[*dialect.SelectQuery]) PublicreportNuisanceOldsQuery { return PublicreportNuisanceOlds.Query(append(mods, @@ -823,6 +848,74 @@ func (address0 *Address) AttachMailers(ctx context.Context, exec bob.Executor, r return nil } +func insertAddressPools0(ctx context.Context, exec bob.Executor, fileuploadPools1 []*FileuploadPoolSetter, address0 *Address) (FileuploadPoolSlice, error) { + for i := range fileuploadPools1 { + fileuploadPools1[i].AddressID = omitnull.From(address0.ID) + } + + ret, err := FileuploadPools.Insert(bob.ToMods(fileuploadPools1...)).All(ctx, exec) + if err != nil { + return ret, fmt.Errorf("insertAddressPools0: %w", err) + } + + return ret, nil +} + +func attachAddressPools0(ctx context.Context, exec bob.Executor, count int, fileuploadPools1 FileuploadPoolSlice, address0 *Address) (FileuploadPoolSlice, error) { + setter := &FileuploadPoolSetter{ + AddressID: omitnull.From(address0.ID), + } + + err := fileuploadPools1.UpdateAll(ctx, exec, *setter) + if err != nil { + return nil, fmt.Errorf("attachAddressPools0: %w", err) + } + + return fileuploadPools1, nil +} + +func (address0 *Address) InsertPools(ctx context.Context, exec bob.Executor, related ...*FileuploadPoolSetter) error { + if len(related) == 0 { + return nil + } + + var err error + + fileuploadPools1, err := insertAddressPools0(ctx, exec, related, address0) + if err != nil { + return err + } + + address0.R.Pools = append(address0.R.Pools, fileuploadPools1...) + + for _, rel := range fileuploadPools1 { + rel.R.Address = address0 + } + return nil +} + +func (address0 *Address) AttachPools(ctx context.Context, exec bob.Executor, related ...*FileuploadPool) error { + if len(related) == 0 { + return nil + } + + var err error + fileuploadPools1 := FileuploadPoolSlice(related) + + _, err = attachAddressPools0(ctx, exec, len(related), fileuploadPools1, address0) + if err != nil { + return err + } + + address0.R.Pools = append(address0.R.Pools, fileuploadPools1...) + + for _, rel := range related { + rel.R.Address = address0 + } + + return nil +} + func insertAddressNuisanceOlds0(ctx context.Context, exec bob.Executor, publicreportNuisanceOlds1 []*PublicreportNuisanceOldSetter, address0 *Address) (PublicreportNuisanceOldSlice, error) { for i := range publicreportNuisanceOlds1 { publicreportNuisanceOlds1[i].AddressID = omitnull.From(address0.ID) @@ -1203,6 +1296,20 @@ func (o *Address) Preload(name string, retrieved any) error { o.R.Mailers = rels + for _, rel := range rels { + if rel != nil { + rel.R.Address = o + } + } + return nil + case "Pools": + rels, ok := retrieved.(FileuploadPoolSlice) + if !ok { + return fmt.Errorf("address cannot load %T as %q", retrieved, name) + } + + o.R.Pools = rels + for _, rel := range rels { if rel != nil { rel.R.Address = o @@ -1306,6 +1413,7 @@ func buildAddressPreloader() addressPreloader { type addressThenLoader[Q orm.Loadable] struct { Mailers func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] + Pools func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] NuisanceOlds func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] Reports func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] WaterOlds func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] @@ -1317,6 +1425,9 @@ func buildAddressThenLoader[Q orm.Loadable]() addressThenLoader[Q] { type MailersLoadInterface interface { LoadMailers(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error } + type PoolsLoadInterface interface { + LoadPools(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } type NuisanceOldsLoadInterface interface { LoadNuisanceOlds(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error } @@ -1340,6 +1451,12 @@ func buildAddressThenLoader[Q orm.Loadable]() addressThenLoader[Q] { return retrieved.LoadMailers(ctx, exec, mods...) }, ), + Pools: thenLoadBuilder[Q]( + "Pools", + func(ctx context.Context, exec bob.Executor, retrieved PoolsLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadPools(ctx, exec, mods...) + }, + ), NuisanceOlds: thenLoadBuilder[Q]( "NuisanceOlds", func(ctx context.Context, exec bob.Executor, retrieved NuisanceOldsLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { @@ -1434,6 +1551,70 @@ func (os AddressSlice) LoadMailers(ctx context.Context, exec bob.Executor, mods return nil } +// LoadPools loads the address's Pools into the .R struct +func (o *Address) LoadPools(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + // Reset the relationship + o.R.Pools = nil + + related, err := o.Pools(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, rel := range related { + rel.R.Address = o + } + + o.R.Pools = related + return nil +} + +// LoadPools loads the address's Pools into the .R struct +func (os AddressSlice) LoadPools(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + fileuploadPools, err := os.Pools(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, o := range os { + if o == nil { + continue + } + + o.R.Pools = nil + } + + for _, o := range os { + if o == nil { + continue + } + + for _, rel := range fileuploadPools { + + if !rel.AddressID.IsValue() { + continue + } + if !(rel.AddressID.IsValue() && o.ID == rel.AddressID.MustGet()) { + continue + } + + rel.R.Address = o + + o.R.Pools = append(o.R.Pools, rel) + } + } + + return nil +} + // LoadNuisanceOlds loads the address's NuisanceOlds into the .R struct func (o *Address) LoadNuisanceOlds(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { if o == nil { diff --git a/db/models/fileupload.pool.bob.go b/db/models/fileupload.pool.bob.go index 95f38ca4..50ac825b 100644 --- a/db/models/fileupload.pool.bob.go +++ b/db/models/fileupload.pool.bob.go @@ -49,6 +49,7 @@ type FileuploadPool struct { AddressLocality string `db:"address_locality" ` AddressRegion string `db:"address_region" ` Condition enums.Poolconditiontype `db:"condition" ` + AddressID null.Val[int32] `db:"address_id" ` R fileuploadPoolR `db:"-" ` } @@ -65,6 +66,7 @@ type FileuploadPoolsQuery = *psql.ViewQuery[*FileuploadPool, FileuploadPoolSlice // fileuploadPoolR is where relationships are stored. type fileuploadPoolR struct { + Address *Address // fileupload.pool.pool_address_id_fkey CreatorUser *User // fileupload.pool.pool_creator_id_fkey CSVFileCSV *FileuploadCSV // fileupload.pool.pool_csv_file_fkey PropertyOwnerPhoneE164Phone *CommsPhone // fileupload.pool.pool_property_owner_phone_e164_fkey @@ -74,7 +76,7 @@ type fileuploadPoolR struct { func buildFileuploadPoolColumns(alias string) fileuploadPoolColumns { return fileuploadPoolColumns{ ColumnsExpr: expr.NewColumnsExpr( - "address_postal_code", "address_street", "committed", "created", "creator_id", "csv_file", "deleted", "geom", "h3cell", "id", "is_in_district", "is_new", "notes", "property_owner_name", "property_owner_phone_e164", "resident_owned", "resident_phone_e164", "line_number", "tags", "address_number", "address_locality", "address_region", "condition", + "address_postal_code", "address_street", "committed", "created", "creator_id", "csv_file", "deleted", "geom", "h3cell", "id", "is_in_district", "is_new", "notes", "property_owner_name", "property_owner_phone_e164", "resident_owned", "resident_phone_e164", "line_number", "tags", "address_number", "address_locality", "address_region", "condition", "address_id", ).WithParent("fileupload.pool"), tableAlias: alias, AddressPostalCode: psql.Quote(alias, "address_postal_code"), @@ -100,6 +102,7 @@ func buildFileuploadPoolColumns(alias string) fileuploadPoolColumns { AddressLocality: psql.Quote(alias, "address_locality"), AddressRegion: psql.Quote(alias, "address_region"), Condition: psql.Quote(alias, "condition"), + AddressID: psql.Quote(alias, "address_id"), } } @@ -129,6 +132,7 @@ type fileuploadPoolColumns struct { AddressLocality psql.Expression AddressRegion psql.Expression Condition psql.Expression + AddressID psql.Expression } func (c fileuploadPoolColumns) Alias() string { @@ -166,10 +170,11 @@ type FileuploadPoolSetter struct { AddressLocality omit.Val[string] `db:"address_locality" ` AddressRegion omit.Val[string] `db:"address_region" ` Condition omit.Val[enums.Poolconditiontype] `db:"condition" ` + AddressID omitnull.Val[int32] `db:"address_id" ` } func (s FileuploadPoolSetter) SetColumns() []string { - vals := make([]string, 0, 23) + vals := make([]string, 0, 24) if s.AddressPostalCode.IsValue() { vals = append(vals, "address_postal_code") } @@ -239,6 +244,9 @@ func (s FileuploadPoolSetter) SetColumns() []string { if s.Condition.IsValue() { vals = append(vals, "condition") } + if !s.AddressID.IsUnset() { + vals = append(vals, "address_id") + } return vals } @@ -312,6 +320,9 @@ func (s FileuploadPoolSetter) Overwrite(t *FileuploadPool) { if s.Condition.IsValue() { t.Condition = s.Condition.MustGet() } + if !s.AddressID.IsUnset() { + t.AddressID = s.AddressID.MustGetNull() + } } func (s *FileuploadPoolSetter) Apply(q *dialect.InsertQuery) { @@ -320,7 +331,7 @@ func (s *FileuploadPoolSetter) Apply(q *dialect.InsertQuery) { }) q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { - vals := make([]bob.Expression, 23) + vals := make([]bob.Expression, 24) if s.AddressPostalCode.IsValue() { vals[0] = psql.Arg(s.AddressPostalCode.MustGet()) } else { @@ -459,6 +470,12 @@ func (s *FileuploadPoolSetter) Apply(q *dialect.InsertQuery) { vals[22] = psql.Raw("DEFAULT") } + if !s.AddressID.IsUnset() { + vals[23] = psql.Arg(s.AddressID.MustGetNull()) + } else { + vals[23] = psql.Raw("DEFAULT") + } + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") })) } @@ -468,7 +485,7 @@ func (s FileuploadPoolSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { } func (s FileuploadPoolSetter) Expressions(prefix ...string) []bob.Expression { - exprs := make([]bob.Expression, 0, 23) + exprs := make([]bob.Expression, 0, 24) if s.AddressPostalCode.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ @@ -631,6 +648,13 @@ func (s FileuploadPoolSetter) Expressions(prefix ...string) []bob.Expression { }}) } + if !s.AddressID.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "address_id")...), + psql.Arg(s.AddressID), + }}) + } + return exprs } @@ -857,6 +881,30 @@ func (o FileuploadPoolSlice) ReloadAll(ctx context.Context, exec bob.Executor) e return nil } +// Address starts a query for related objects on address +func (o *FileuploadPool) Address(mods ...bob.Mod[*dialect.SelectQuery]) AddressesQuery { + return Addresses.Query(append(mods, + sm.Where(Addresses.Columns.ID.EQ(psql.Arg(o.AddressID))), + )...) +} + +func (os FileuploadPoolSlice) Address(mods ...bob.Mod[*dialect.SelectQuery]) AddressesQuery { + pkAddressID := make(pgtypes.Array[null.Val[int32]], 0, len(os)) + for _, o := range os { + if o == nil { + continue + } + pkAddressID = append(pkAddressID, o.AddressID) + } + PKArgExpr := psql.Select(sm.Columns( + psql.F("unnest", psql.Cast(psql.Arg(pkAddressID), "integer[]")), + )) + + return Addresses.Query(append(mods, + sm.Where(psql.Group(Addresses.Columns.ID).OP("IN", PKArgExpr)), + )...) +} + // CreatorUser starts a query for related objects on user_ func (o *FileuploadPool) CreatorUser(mods ...bob.Mod[*dialect.SelectQuery]) UsersQuery { return Users.Query(append(mods, @@ -953,6 +1001,54 @@ func (os FileuploadPoolSlice) ResidentPhoneE164Phone(mods ...bob.Mod[*dialect.Se )...) } +func attachFileuploadPoolAddress0(ctx context.Context, exec bob.Executor, count int, fileuploadPool0 *FileuploadPool, address1 *Address) (*FileuploadPool, error) { + setter := &FileuploadPoolSetter{ + AddressID: omitnull.From(address1.ID), + } + + err := fileuploadPool0.Update(ctx, exec, setter) + if err != nil { + return nil, fmt.Errorf("attachFileuploadPoolAddress0: %w", err) + } + + return fileuploadPool0, nil +} + +func (fileuploadPool0 *FileuploadPool) InsertAddress(ctx context.Context, exec bob.Executor, related *AddressSetter) error { + var err error + + address1, err := Addresses.Insert(related).One(ctx, exec) + if err != nil { + return fmt.Errorf("inserting related objects: %w", err) + } + + _, err = attachFileuploadPoolAddress0(ctx, exec, 1, fileuploadPool0, address1) + if err != nil { + return err + } + + fileuploadPool0.R.Address = address1 + + address1.R.Pools = append(address1.R.Pools, fileuploadPool0) + + return nil +} + +func (fileuploadPool0 *FileuploadPool) AttachAddress(ctx context.Context, exec bob.Executor, address1 *Address) error { + var err error + + _, err = attachFileuploadPoolAddress0(ctx, exec, 1, fileuploadPool0, address1) + if err != nil { + return err + } + + fileuploadPool0.R.Address = address1 + + address1.R.Pools = append(address1.R.Pools, fileuploadPool0) + + return nil +} + func attachFileuploadPoolCreatorUser0(ctx context.Context, exec bob.Executor, count int, fileuploadPool0 *FileuploadPool, user1 *User) (*FileuploadPool, error) { setter := &FileuploadPoolSetter{ CreatorID: omit.From(user1.ID), @@ -1169,6 +1265,7 @@ type fileuploadPoolWhere[Q psql.Filterable] struct { AddressLocality psql.WhereMod[Q, string] AddressRegion psql.WhereMod[Q, string] Condition psql.WhereMod[Q, enums.Poolconditiontype] + AddressID psql.WhereNullMod[Q, int32] } func (fileuploadPoolWhere[Q]) AliasedAs(alias string) fileuploadPoolWhere[Q] { @@ -1200,6 +1297,7 @@ func buildFileuploadPoolWhere[Q psql.Filterable](cols fileuploadPoolColumns) fil AddressLocality: psql.Where[Q, string](cols.AddressLocality), AddressRegion: psql.Where[Q, string](cols.AddressRegion), Condition: psql.Where[Q, enums.Poolconditiontype](cols.Condition), + AddressID: psql.WhereNull[Q, int32](cols.AddressID), } } @@ -1209,6 +1307,18 @@ func (o *FileuploadPool) Preload(name string, retrieved any) error { } switch name { + case "Address": + rel, ok := retrieved.(*Address) + if !ok { + return fmt.Errorf("fileuploadPool cannot load %T as %q", retrieved, name) + } + + o.R.Address = rel + + if rel != nil { + rel.R.Pools = FileuploadPoolSlice{o} + } + return nil case "CreatorUser": rel, ok := retrieved.(*User) if !ok { @@ -1263,6 +1373,7 @@ func (o *FileuploadPool) Preload(name string, retrieved any) error { } type fileuploadPoolPreloader struct { + Address func(...psql.PreloadOption) psql.Preloader CreatorUser func(...psql.PreloadOption) psql.Preloader CSVFileCSV func(...psql.PreloadOption) psql.Preloader PropertyOwnerPhoneE164Phone func(...psql.PreloadOption) psql.Preloader @@ -1271,6 +1382,19 @@ type fileuploadPoolPreloader struct { func buildFileuploadPoolPreloader() fileuploadPoolPreloader { return fileuploadPoolPreloader{ + Address: func(opts ...psql.PreloadOption) psql.Preloader { + return psql.Preload[*Address, AddressSlice](psql.PreloadRel{ + Name: "Address", + Sides: []psql.PreloadSide{ + { + From: FileuploadPools, + To: Addresses, + FromColumns: []string{"address_id"}, + ToColumns: []string{"id"}, + }, + }, + }, Addresses.Columns.Names(), opts...) + }, CreatorUser: func(opts ...psql.PreloadOption) psql.Preloader { return psql.Preload[*User, UserSlice](psql.PreloadRel{ Name: "CreatorUser", @@ -1327,6 +1451,7 @@ func buildFileuploadPoolPreloader() fileuploadPoolPreloader { } type fileuploadPoolThenLoader[Q orm.Loadable] struct { + Address func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] CreatorUser func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] CSVFileCSV func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] PropertyOwnerPhoneE164Phone func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] @@ -1334,6 +1459,9 @@ type fileuploadPoolThenLoader[Q orm.Loadable] struct { } func buildFileuploadPoolThenLoader[Q orm.Loadable]() fileuploadPoolThenLoader[Q] { + type AddressLoadInterface interface { + LoadAddress(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } type CreatorUserLoadInterface interface { LoadCreatorUser(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error } @@ -1348,6 +1476,12 @@ func buildFileuploadPoolThenLoader[Q orm.Loadable]() fileuploadPoolThenLoader[Q] } return fileuploadPoolThenLoader[Q]{ + Address: thenLoadBuilder[Q]( + "Address", + func(ctx context.Context, exec bob.Executor, retrieved AddressLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadAddress(ctx, exec, mods...) + }, + ), CreatorUser: thenLoadBuilder[Q]( "CreatorUser", func(ctx context.Context, exec bob.Executor, retrieved CreatorUserLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { @@ -1375,6 +1509,61 @@ func buildFileuploadPoolThenLoader[Q orm.Loadable]() fileuploadPoolThenLoader[Q] } } +// LoadAddress loads the fileuploadPool's Address into the .R struct +func (o *FileuploadPool) LoadAddress(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + // Reset the relationship + o.R.Address = nil + + related, err := o.Address(mods...).One(ctx, exec) + if err != nil { + return err + } + + related.R.Pools = FileuploadPoolSlice{o} + + o.R.Address = related + return nil +} + +// LoadAddress loads the fileuploadPool's Address into the .R struct +func (os FileuploadPoolSlice) LoadAddress(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + addresses, err := os.Address(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, o := range os { + if o == nil { + continue + } + + for _, rel := range addresses { + if !o.AddressID.IsValue() { + continue + } + + if !(o.AddressID.IsValue() && o.AddressID.MustGet() == rel.ID) { + continue + } + + rel.R.Pools = append(rel.R.Pools, o) + + o.R.Address = rel + break + } + } + + return nil +} + // LoadCreatorUser loads the fileuploadPool's CreatorUser into the .R struct func (o *FileuploadPool) LoadCreatorUser(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { if o == nil { diff --git a/platform/csv/pool.go b/platform/csv/pool.go index b81f079d..f3fea183 100644 --- a/platform/csv/pool.go +++ b/platform/csv/pool.go @@ -161,6 +161,7 @@ func geocodePool(ctx context.Context, txn bob.Tx, client *stadia.StadiaMaps, job um.Table("fileupload.pool"), um.SetCol("h3cell").ToArg(geo.Cell), um.SetCol("geom").To(geom_query), + um.SetCol("address_id").To(*geo.Address.ID), um.Where(psql.Quote("id").EQ(psql.Arg(pool.ID))), ).Exec(ctx, txn) if err != nil { diff --git a/platform/geocode/address.go b/platform/geocode/address.go index d4065ef4..2d227aaf 100644 --- a/platform/geocode/address.go +++ b/platform/geocode/address.go @@ -9,6 +9,7 @@ import ( "github.com/Gleipnir-Technology/bob/dialect/psql" "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" "github.com/Gleipnir-Technology/bob/dialect/psql/im" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" //bobtypes "github.com/Gleipnir-Technology/bob/types" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/h3utils" @@ -124,9 +125,11 @@ func insertAddress(ctx context.Context, txn bob.Executor, address types.Address) } return &row.ID, nil } -func insertAddresses(ctx context.Context, txn bob.Executor, features []stadia.GeocodeFeature) ([]int32, error) { +func insertAddresses(ctx context.Context, txn bob.Executor, features []stadia.GeocodeFeature) ([]types.Address, error) { query := addressQuery() - for _, feature := range features { + gids := make([]string, len(features)) + for i, feature := range features { + gids[i] = feature.Properties.GID lng := feature.Geometry.Coordinates[0] lat := feature.Geometry.Coordinates[1] cell, err := h3utils.GetCell(lng, lat, 15) @@ -148,15 +151,24 @@ func insertAddresses(ctx context.Context, txn bob.Executor, features []stadia.Ge psql.Arg(feature.Street()), psql.Raw("''"), ), + im.OnConflict("gid").DoNothing(), ) } - rows, err := bob.All(ctx, txn, query, scan.StructMapper[_rowWithID]()) + _, err := bob.All(ctx, txn, query, scan.StructMapper[_rowWithID]()) if err != nil { return nil, fmt.Errorf("insert: %w", err) } - results := make([]int32, len(rows)) - for _, row := range rows { - results = append(results, row.ID) + addresses, err := models.Addresses.Query( + sm.Where( + models.Addresses.Columns.Gid.EQ(psql.Any(gids)), + ), + ).All(ctx, txn) + if err != nil { + return nil, fmt.Errorf("query by gid: %w", err) + } + results := make([]types.Address, len(addresses)) + for i, address := range addresses { + results[i] = types.AddressFromModel(address) } return results, nil } diff --git a/platform/geocode/geocode.go b/platform/geocode/geocode.go index dd500f8f..597c66d2 100644 --- a/platform/geocode/geocode.go +++ b/platform/geocode/geocode.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "strings" "time" "github.com/Gleipnir-Technology/bob" @@ -58,8 +57,11 @@ func GeocodeRaw(ctx context.Context, org *models.Organization, address string) ( if err != nil { return nil, fmt.Errorf("client raw geocode failure on %s: %w", address, err) } - insertAddresses(ctx, db.PGInstance.BobDB, resp.Features) - return toGeocodeResult(*resp, address) + addresses, err := insertAddresses(ctx, db.PGInstance.BobDB, resp.Features) + if err != nil { + return nil, fmt.Errorf("insert addresses: %w", err) + } + return toGeocodeResult(*resp, address, addresses) } func GeocodeStructured(ctx context.Context, org *models.Organization, a types.Address) (*GeocodeResult, error) { street := fmt.Sprintf("%s %s", a.Number, a.Street) @@ -75,8 +77,11 @@ func GeocodeStructured(ctx context.Context, org *models.Organization, a types.Ad if err != nil { return nil, fmt.Errorf("client structured geocode failure on %s: %w", a.String(), err) } - insertAddresses(ctx, db.PGInstance.BobDB, resp.Features) - return toGeocodeResult(*resp, a.String()) + addresses, err := insertAddresses(ctx, db.PGInstance.BobDB, resp.Features) + if err != nil { + return nil, fmt.Errorf("insert addresses: %w", err) + } + return toGeocodeResult(*resp, a.String(), addresses) } func ReverseGeocode(ctx context.Context, location types.Location) (*GeocodeResult, error) { req := stadia.RequestReverseGeocode{ @@ -87,54 +92,33 @@ func ReverseGeocode(ctx context.Context, location types.Location) (*GeocodeResul if err != nil { return nil, fmt.Errorf("client reverse geocode failure on %s: %w", location.String(), err) } - insertAddresses(ctx, db.PGInstance.BobDB, resp.Features) - return toGeocodeResult(*resp, location.String()) + addresses, err := insertAddresses(ctx, db.PGInstance.BobDB, resp.Features) + if err != nil { + return nil, fmt.Errorf("insert addresses: %w", err) + } + return toGeocodeResult(*resp, location.String(), addresses) } -func toGeocodeResult(resp stadia.GeocodeResponse, address_msg string) (*GeocodeResult, error) { +func toGeocodeResult(resp stadia.GeocodeResponse, address_msg string, addresses []types.Address) (*GeocodeResult, error) { if len(resp.Features) < 1 { return nil, fmt.Errorf("%s matched no locations", address_msg) } - feature := resp.Features[0] + if len(addresses) < 1 { + return nil, fmt.Errorf("no addresses") + } if len(resp.Features) > 1 { if !allFeaturesIdenticalEnough(resp.Features) { return nil, fmt.Errorf("%s matched more than one location, and they differ a lot", address_msg) } } + feature := resp.Features[0] + address := addresses[0] if feature.Geometry.Type != "Point" { return nil, fmt.Errorf("wrong type %s from %s", feature.Geometry.Type, address_msg) } - longitude := feature.Geometry.Coordinates[0] - latitude := feature.Geometry.Coordinates[1] - cell, err := h3utils.GetCell(longitude, latitude, 15) + cell, err := h3utils.GetCell(address.Location.Longitude, address.Location.Latitude, 15) if err != nil { - return nil, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", longitude, latitude) - } - country_s := strings.ToLower(feature.Properties.CountryA) - // Depending on what kind of request we made we'll get wildly different result structures - // This first structure generally works for forword geocoding - address := types.Address{ - Country: country_s, - GID: feature.Properties.GID, - Locality: feature.Properties.Locality, - Location: &types.Location{ - Longitude: feature.Geometry.Coordinates[0], - Latitude: feature.Geometry.Coordinates[1], - }, - Number: feature.Properties.HouseNumber, - PostalCode: feature.Properties.PostalCode, - Region: feature.Properties.Region, - Raw: feature.Properties.FormattedAddressLine, - Street: feature.Properties.Street, - Unit: "", - } - // If we don't have a locality, try populating for reverse geocoding - if address.Country == "" { - address.Country = strings.ToLower(feature.Properties.Context.ISO3166A3) - address.Locality = feature.Properties.Context.WhosOnFirst.Locality.Name - address.Number = feature.Properties.AddressComponents.Number - address.PostalCode = feature.Properties.AddressComponents.PostalCode - address.Street = feature.Properties.AddressComponents.Street + return nil, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", address.Location.Longitude, address.Location.Latitude) } return &GeocodeResult{ Address: address, diff --git a/ts/view/configuration/UploadDetail.vue b/ts/view/configuration/UploadDetail.vue index d4c352c3..9ac15b6b 100644 --- a/ts/view/configuration/UploadDetail.vue +++ b/ts/view/configuration/UploadDetail.vue @@ -268,7 +268,11 @@ tr.has-error { {{ titleCase(pool.condition) }} - {{ pool.tags?.size || 0 }} + + +