From b09063f74a46c0b160980ca6e401657114ad09e7 Mon Sep 17 00:00:00 2001 From: John Costa Date: Tue, 18 Mar 2025 17:48:38 +0000 Subject: [PATCH] refactor(text,tags,links): to foreign key to image instead of user_image --- .../.gen/haystack/haystack/model/events.go | 19 ++++ .../haystack/haystack/model/image_events.go | 18 ++++ .../haystack/model/image_locations.go | 18 ++++ .../.gen/haystack/haystack/model/locations.go | 20 +++++ .../.gen/haystack/haystack/table/events.go | 84 ++++++++++++++++++ .../haystack/haystack/table/image_events.go | 81 +++++++++++++++++ .../haystack/table/image_locations.go | 81 +++++++++++++++++ .../.gen/haystack/haystack/table/locations.go | 87 +++++++++++++++++++ .../haystack/table/table_use_schema.go | 4 + backend/main.go | 22 ++++- backend/models/image.go | 4 + backend/models/locations.go | 29 +++++++ backend/models/user.go | 22 +++-- backend/schema.sql | 36 +++++++- 14 files changed, 510 insertions(+), 15 deletions(-) create mode 100644 backend/.gen/haystack/haystack/model/events.go create mode 100644 backend/.gen/haystack/haystack/model/image_events.go create mode 100644 backend/.gen/haystack/haystack/model/image_locations.go create mode 100644 backend/.gen/haystack/haystack/model/locations.go create mode 100644 backend/.gen/haystack/haystack/table/events.go create mode 100644 backend/.gen/haystack/haystack/table/image_events.go create mode 100644 backend/.gen/haystack/haystack/table/image_locations.go create mode 100644 backend/.gen/haystack/haystack/table/locations.go create mode 100644 backend/models/locations.go diff --git a/backend/.gen/haystack/haystack/model/events.go b/backend/.gen/haystack/haystack/model/events.go new file mode 100644 index 0000000..a9494cb --- /dev/null +++ b/backend/.gen/haystack/haystack/model/events.go @@ -0,0 +1,19 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type Events struct { + ID uuid.UUID `sql:"primary_key"` + Name string + Description *string + Location *uuid.UUID +} diff --git a/backend/.gen/haystack/haystack/model/image_events.go b/backend/.gen/haystack/haystack/model/image_events.go new file mode 100644 index 0000000..66cc398 --- /dev/null +++ b/backend/.gen/haystack/haystack/model/image_events.go @@ -0,0 +1,18 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type ImageEvents struct { + ID uuid.UUID `sql:"primary_key"` + EventID uuid.UUID + ImageID uuid.UUID +} diff --git a/backend/.gen/haystack/haystack/model/image_locations.go b/backend/.gen/haystack/haystack/model/image_locations.go new file mode 100644 index 0000000..8feca46 --- /dev/null +++ b/backend/.gen/haystack/haystack/model/image_locations.go @@ -0,0 +1,18 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type ImageLocations struct { + ID uuid.UUID `sql:"primary_key"` + LocationID uuid.UUID + ImageID uuid.UUID +} diff --git a/backend/.gen/haystack/haystack/model/locations.go b/backend/.gen/haystack/haystack/model/locations.go new file mode 100644 index 0000000..c91c521 --- /dev/null +++ b/backend/.gen/haystack/haystack/model/locations.go @@ -0,0 +1,20 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type Locations struct { + ID uuid.UUID `sql:"primary_key"` + Name string + Address *string + Coordinates *string + Description *string +} diff --git a/backend/.gen/haystack/haystack/table/events.go b/backend/.gen/haystack/haystack/table/events.go new file mode 100644 index 0000000..7d1eab5 --- /dev/null +++ b/backend/.gen/haystack/haystack/table/events.go @@ -0,0 +1,84 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var Events = newEventsTable("haystack", "events", "") + +type eventsTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + Name postgres.ColumnString + Description postgres.ColumnString + Location postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type EventsTable struct { + eventsTable + + EXCLUDED eventsTable +} + +// AS creates new EventsTable with assigned alias +func (a EventsTable) AS(alias string) *EventsTable { + return newEventsTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new EventsTable with assigned schema name +func (a EventsTable) FromSchema(schemaName string) *EventsTable { + return newEventsTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new EventsTable with assigned table prefix +func (a EventsTable) WithPrefix(prefix string) *EventsTable { + return newEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new EventsTable with assigned table suffix +func (a EventsTable) WithSuffix(suffix string) *EventsTable { + return newEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newEventsTable(schemaName, tableName, alias string) *EventsTable { + return &EventsTable{ + eventsTable: newEventsTableImpl(schemaName, tableName, alias), + EXCLUDED: newEventsTableImpl("", "excluded", ""), + } +} + +func newEventsTableImpl(schemaName, tableName, alias string) eventsTable { + var ( + IDColumn = postgres.StringColumn("id") + NameColumn = postgres.StringColumn("name") + DescriptionColumn = postgres.StringColumn("description") + LocationColumn = postgres.StringColumn("location") + allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, LocationColumn} + mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, LocationColumn} + ) + + return eventsTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + Name: NameColumn, + Description: DescriptionColumn, + Location: LocationColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/.gen/haystack/haystack/table/image_events.go b/backend/.gen/haystack/haystack/table/image_events.go new file mode 100644 index 0000000..385cce2 --- /dev/null +++ b/backend/.gen/haystack/haystack/table/image_events.go @@ -0,0 +1,81 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var ImageEvents = newImageEventsTable("haystack", "image_events", "") + +type imageEventsTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + EventID postgres.ColumnString + ImageID postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type ImageEventsTable struct { + imageEventsTable + + EXCLUDED imageEventsTable +} + +// AS creates new ImageEventsTable with assigned alias +func (a ImageEventsTable) AS(alias string) *ImageEventsTable { + return newImageEventsTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new ImageEventsTable with assigned schema name +func (a ImageEventsTable) FromSchema(schemaName string) *ImageEventsTable { + return newImageEventsTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new ImageEventsTable with assigned table prefix +func (a ImageEventsTable) WithPrefix(prefix string) *ImageEventsTable { + return newImageEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new ImageEventsTable with assigned table suffix +func (a ImageEventsTable) WithSuffix(suffix string) *ImageEventsTable { + return newImageEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newImageEventsTable(schemaName, tableName, alias string) *ImageEventsTable { + return &ImageEventsTable{ + imageEventsTable: newImageEventsTableImpl(schemaName, tableName, alias), + EXCLUDED: newImageEventsTableImpl("", "excluded", ""), + } +} + +func newImageEventsTableImpl(schemaName, tableName, alias string) imageEventsTable { + var ( + IDColumn = postgres.StringColumn("id") + EventIDColumn = postgres.StringColumn("event_id") + ImageIDColumn = postgres.StringColumn("image_id") + allColumns = postgres.ColumnList{IDColumn, EventIDColumn, ImageIDColumn} + mutableColumns = postgres.ColumnList{EventIDColumn, ImageIDColumn} + ) + + return imageEventsTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + EventID: EventIDColumn, + ImageID: ImageIDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/.gen/haystack/haystack/table/image_locations.go b/backend/.gen/haystack/haystack/table/image_locations.go new file mode 100644 index 0000000..70cf057 --- /dev/null +++ b/backend/.gen/haystack/haystack/table/image_locations.go @@ -0,0 +1,81 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var ImageLocations = newImageLocationsTable("haystack", "image_locations", "") + +type imageLocationsTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + LocationID postgres.ColumnString + ImageID postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type ImageLocationsTable struct { + imageLocationsTable + + EXCLUDED imageLocationsTable +} + +// AS creates new ImageLocationsTable with assigned alias +func (a ImageLocationsTable) AS(alias string) *ImageLocationsTable { + return newImageLocationsTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new ImageLocationsTable with assigned schema name +func (a ImageLocationsTable) FromSchema(schemaName string) *ImageLocationsTable { + return newImageLocationsTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new ImageLocationsTable with assigned table prefix +func (a ImageLocationsTable) WithPrefix(prefix string) *ImageLocationsTable { + return newImageLocationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new ImageLocationsTable with assigned table suffix +func (a ImageLocationsTable) WithSuffix(suffix string) *ImageLocationsTable { + return newImageLocationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newImageLocationsTable(schemaName, tableName, alias string) *ImageLocationsTable { + return &ImageLocationsTable{ + imageLocationsTable: newImageLocationsTableImpl(schemaName, tableName, alias), + EXCLUDED: newImageLocationsTableImpl("", "excluded", ""), + } +} + +func newImageLocationsTableImpl(schemaName, tableName, alias string) imageLocationsTable { + var ( + IDColumn = postgres.StringColumn("id") + LocationIDColumn = postgres.StringColumn("location_id") + ImageIDColumn = postgres.StringColumn("image_id") + allColumns = postgres.ColumnList{IDColumn, LocationIDColumn, ImageIDColumn} + mutableColumns = postgres.ColumnList{LocationIDColumn, ImageIDColumn} + ) + + return imageLocationsTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + LocationID: LocationIDColumn, + ImageID: ImageIDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/.gen/haystack/haystack/table/locations.go b/backend/.gen/haystack/haystack/table/locations.go new file mode 100644 index 0000000..0664d79 --- /dev/null +++ b/backend/.gen/haystack/haystack/table/locations.go @@ -0,0 +1,87 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var Locations = newLocationsTable("haystack", "locations", "") + +type locationsTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + Name postgres.ColumnString + Address postgres.ColumnString + Coordinates postgres.ColumnString + Description postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type LocationsTable struct { + locationsTable + + EXCLUDED locationsTable +} + +// AS creates new LocationsTable with assigned alias +func (a LocationsTable) AS(alias string) *LocationsTable { + return newLocationsTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new LocationsTable with assigned schema name +func (a LocationsTable) FromSchema(schemaName string) *LocationsTable { + return newLocationsTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new LocationsTable with assigned table prefix +func (a LocationsTable) WithPrefix(prefix string) *LocationsTable { + return newLocationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new LocationsTable with assigned table suffix +func (a LocationsTable) WithSuffix(suffix string) *LocationsTable { + return newLocationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newLocationsTable(schemaName, tableName, alias string) *LocationsTable { + return &LocationsTable{ + locationsTable: newLocationsTableImpl(schemaName, tableName, alias), + EXCLUDED: newLocationsTableImpl("", "excluded", ""), + } +} + +func newLocationsTableImpl(schemaName, tableName, alias string) locationsTable { + var ( + IDColumn = postgres.StringColumn("id") + NameColumn = postgres.StringColumn("name") + AddressColumn = postgres.StringColumn("address") + CoordinatesColumn = postgres.StringColumn("coordinates") + DescriptionColumn = postgres.StringColumn("description") + allColumns = postgres.ColumnList{IDColumn, NameColumn, AddressColumn, CoordinatesColumn, DescriptionColumn} + mutableColumns = postgres.ColumnList{NameColumn, AddressColumn, CoordinatesColumn, DescriptionColumn} + ) + + return locationsTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + Name: NameColumn, + Address: AddressColumn, + Coordinates: CoordinatesColumn, + Description: DescriptionColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/.gen/haystack/haystack/table/table_use_schema.go b/backend/.gen/haystack/haystack/table/table_use_schema.go index 913cd2a..8f5d510 100644 --- a/backend/.gen/haystack/haystack/table/table_use_schema.go +++ b/backend/.gen/haystack/haystack/table/table_use_schema.go @@ -10,10 +10,14 @@ package table // UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke // this method only once at the beginning of the program. func UseSchema(schema string) { + Events = Events.FromSchema(schema) Image = Image.FromSchema(schema) + ImageEvents = ImageEvents.FromSchema(schema) ImageLinks = ImageLinks.FromSchema(schema) + ImageLocations = ImageLocations.FromSchema(schema) ImageTags = ImageTags.FromSchema(schema) ImageText = ImageText.FromSchema(schema) + Locations = Locations.FromSchema(schema) UserImages = UserImages.FromSchema(schema) UserImagesToProcess = UserImagesToProcess.FromSchema(schema) UserTags = UserTags.FromSchema(schema) diff --git a/backend/main.go b/backend/main.go index 7749c84..3e5ee87 100644 --- a/backend/main.go +++ b/backend/main.go @@ -94,31 +94,45 @@ func main() { image, err := imageModel.GetToProcessWithData(ctx, imageId) if err != nil { + log.Println("Failed to GetToProcessWithData") + log.Println(err) return } imageInfo, err := openAiClient.GetImageInfo(image.Image.ImageName, image.Image.Image) if err != nil { + log.Println("Failed to GetToProcessWithData") + log.Println(err) return } - savedImage, err := imageModel.FinishProcessing(ctx, image.ID) + userImage, err := imageModel.FinishProcessing(ctx, image.ID) if err != nil { + log.Println("Failed to FinishProcessing") + log.Println(err) return } - err = tagModel.SaveToImage(ctx, savedImage.ID, imageInfo.Tags) + log.Println(userImage) + + err = tagModel.SaveToImage(ctx, userImage.ImageID, imageInfo.Tags) if err != nil { + log.Println("Failed to save tags") + log.Println(err) return } - err = linkModel.Save(ctx, savedImage.ID, imageInfo.Links) + err = linkModel.Save(ctx, userImage.ImageID, imageInfo.Links) if err != nil { + log.Println("Failed to save links") + log.Println(err) return } - err = textModel.Save(ctx, savedImage.ID, imageInfo.Text) + err = textModel.Save(ctx, userImage.ImageID, imageInfo.Text) if err != nil { + log.Println("Failed to save text") + log.Println(err) return } }() diff --git a/backend/models/image.go b/backend/models/image.go index a005954..894c579 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -122,7 +122,11 @@ func (m ImageModel) FinishProcessing(ctx context.Context, imageId uuid.UUID) (mo WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID))) _, err = removeProcessingStmt.ExecContext(ctx, tx) + if err != nil { + return model.UserImages{}, err + } + err = tx.Commit() return userImage, err } diff --git a/backend/models/locations.go b/backend/models/locations.go new file mode 100644 index 0000000..fbac5dc --- /dev/null +++ b/backend/models/locations.go @@ -0,0 +1,29 @@ +package models + +import ( + "context" + "database/sql" + "screenmark/screenmark/.gen/haystack/haystack/model" + . "screenmark/screenmark/.gen/haystack/haystack/table" +) + +type LocationModel struct { + dbPool *sql.DB +} + +func (m LocationModel) Save(ctx context.Context, location model.Locations) (model.Locations, error) { + insertLocationStmt := Locations. + INSERT(Locations.Name, Locations.Address, Locations.Coordinates, Locations.Description). + VALUES(location.Name, location.Address, location.Coordinates, location.Description). + RETURNING(Locations.AllColumns) + + insertedLocation := model.Locations{} + + err := insertLocationStmt.QueryContext(ctx, m.dbPool, &insertLocationStmt) + + return insertedLocation, err +} + +func NewLocationModel(db *sql.DB) LocationModel { + return LocationModel{dbPool: db} +} diff --git a/backend/models/user.go b/backend/models/user.go index 3085f2f..18b88a2 100644 --- a/backend/models/user.go +++ b/backend/models/user.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "log" "screenmark/screenmark/.gen/haystack/haystack/model" . "screenmark/screenmark/.gen/haystack/haystack/table" @@ -27,22 +28,26 @@ type ImageWithProperties struct { } Links []model.ImageLinks Text []model.ImageText + + Locations []model.Locations } func getUserIdFromImage(ctx context.Context, dbPool *sql.DB, imageId uuid.UUID) (uuid.UUID, error) { - getUserIdStmt := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ID.EQ(UUID(imageId))) + getUserIdStmt := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ImageID.EQ(UUID(imageId))) - user := []model.Users{} - err := getUserIdStmt.QueryContext(ctx, dbPool, &user) + log.Println(getUserIdStmt.DebugSql()) + + userImages := []model.UserImages{} + err := getUserIdStmt.QueryContext(ctx, dbPool, &userImages) if err != nil { return uuid.Nil, err } - if len(user) != 1 { + if len(userImages) != 1 { return uuid.Nil, errors.New("Expected exactly one choice.") } - return user[0].ID, nil + return userImages[0].UserID, nil } func (m UserModel) ListWithProperties(ctx context.Context, userId uuid.UUID) ([]ImageWithProperties, error) { @@ -56,10 +61,11 @@ func (m UserModel) ListWithProperties(ctx context.Context, userId uuid.UUID) ([] ImageLinks.AllColumns). FROM( UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)). - LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(UserImages.ID)). + LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(Image.ID)). INNER_JOIN(UserTags, UserTags.ID.EQ(ImageTags.TagID)). - LEFT_JOIN(ImageText, ImageText.ImageID.EQ(UserImages.ID)). - LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(UserImages.ID))). + LEFT_JOIN(ImageText, ImageText.ImageID.EQ(Image.ID)). + LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(Image.ID)). + LEFT_JOIN(ImageLocations, ImageLocations.ImageID.EQ(Image.ID))). WHERE(UserImages.UserID.EQ(UUID(userId))) fmt.Println(listWithPropertiesStmt.DebugSql()) diff --git a/backend/schema.sql b/backend/schema.sql index af3d517..a232e5e 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -38,19 +38,49 @@ CREATE TABLE haystack.user_tags ( CREATE TABLE haystack.image_tags ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), tag_id UUID NOT NULL REFERENCES haystack.user_tags (id), - image_id UUID NOT NULL REFERENCES haystack.user_images (id) + image_id UUID NOT NULL REFERENCES haystack.image (id) ); CREATE TABLE haystack.image_text ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), image_text TEXT NOT NULL, - image_id UUID NOT NULL REFERENCES haystack.user_images (id) + image_id UUID NOT NULL REFERENCES haystack.image (id) ); CREATE TABLE haystack.image_links ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), link TEXT NOT NULL, - image_id UUID NOT NULL REFERENCES haystack.user_images (id) + image_id UUID NOT NULL REFERENCES haystack.image (id) +); + +CREATE TABLE haystack.locations ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + address TEXT, + coordinates TEXT, -- Horrible for now. GoJet doesnt support custom types. + description TEXT +); + +CREATE TABLE haystack.image_locations ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + location_id UUID NOT NULL REFERENCES haystack.image_locations (id), + image_id UUID NOT NULL REFERENCES haystack.image (id) +); + +CREATE TABLE haystack.events ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + + -- It seems name and description are frequent. We could use table inheritance. + name TEXT NOT NULL, + description TEXT, + + location UUID REFERENCES haystack.locations (id) +); + +CREATE TABLE haystack.image_events ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + event_id UUID NOT NULL REFERENCES haystack.events (id), + image_id UUID NOT NULL REFERENCES haystack.image (id) ); /* -----| Indexes |----- */