From de6da2906f770ea53cdb8b3aedeaa59c0cc3679b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Mon, 1 Sep 2025 22:41:21 +0200 Subject: [PATCH] added booking types + working on overtime --- .gitattributes | 2 + Backend/endpoints/time-create.go | 3 +- Backend/endpoints/time.go | 4 +- Backend/helper/time_test.go | 2 +- Backend/models/booking.go | 75 ++++++++++++++----- .../models/{controlTables.go => database.go} | 2 +- Backend/models/user.go | 4 +- DB/initdb/01_schema.sql | 2 +- Docker/docker-compose.dev.yml | 2 +- db.sql | 48 ++++++++++++ migrations/20250901201159_initial.down.sql | 16 ++++ migrations/20250901201159_initial.up.sql | 57 ++++++++++++++ .../20250901201250_control_tables.down.sql | 31 ++++++++ .../20250901201250_control_tables.up.sql | 38 ++++++++++ .../20250901201710_triggers_extension.sql | 21 ++++++ migrations/atlas.sum | 6 ++ 16 files changed, 284 insertions(+), 29 deletions(-) create mode 100644 .gitattributes rename Backend/models/{controlTables.go => database.go} (90%) create mode 100644 migrations/20250901201159_initial.down.sql create mode 100644 migrations/20250901201159_initial.up.sql create mode 100644 migrations/20250901201250_control_tables.down.sql create mode 100644 migrations/20250901201250_control_tables.up.sql create mode 100644 migrations/20250901201710_triggers_extension.sql create mode 100644 migrations/atlas.sum diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d765b0a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Ensure all text files use LF line endings +* text=auto eol=lf diff --git a/Backend/endpoints/time-create.go b/Backend/endpoints/time-create.go index 05edd1c..5f64103 100644 --- a/Backend/endpoints/time-create.go +++ b/Backend/endpoints/time-create.go @@ -50,8 +50,9 @@ func createBooking(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(booking) + return } - w.WriteHeader(http.StatusBadRequest) + http.Error(w, "Cannot verify booking, maybe missing a parameter", http.StatusBadRequest) } func verifyToken(r *http.Request) bool { diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index 0172b4c..70fd422 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -110,11 +110,11 @@ func updateBooking(w http.ResponseWriter, r *http.Request) { return } - newBooking := (*models.Booking).New(nil, user.CardUID, 0, int16(check_in_out)) + newBooking := (*models.Booking).New(nil, user.CardUID, 0, int16(check_in_out), 1) newBooking.Timestamp = timestamp err = newBooking.InsertWithTimestamp() if err != nil { - log.Println("Error inserting booking", err) + log.Printf("Error inserting booking %v -> %v\n", newBooking, err) } case "change": absenceType, err := strconv.Atoi(r.FormValue("absence")) diff --git a/Backend/helper/time_test.go b/Backend/helper/time_test.go index 9593ab6..454d4e3 100644 --- a/Backend/helper/time_test.go +++ b/Backend/helper/time_test.go @@ -8,7 +8,7 @@ import ( func TestGetMonday(t *testing.T) { isMonday, err := time.Parse("2006-01-02", "2025-07-14") notMonday, err := time.Parse("2006-01-02", "2025-07-16") - if err != nil || isMonday == notMonday { + if err != nil || isMonday.Equal(notMonday) { t.Errorf("U stupid? %e", err) } if GetMonday(isMonday) != isMonday || GetMonday(notMonday) != isMonday { diff --git a/Backend/models/booking.go b/Backend/models/booking.go index a739f5b..a293ee5 100644 --- a/Backend/models/booking.go +++ b/Backend/models/booking.go @@ -23,11 +23,12 @@ func (e SameBookingError) Error() string { } type Booking struct { - CardUID string `json:"card_uid"` - GeraetID int16 `json:"geraet_id"` - CheckInOut int16 `json:"check_in_out"` - Timestamp time.Time `json:"timestamp"` - CounterId int `json:"counter_id"` + CardUID string `json:"card_uid"` + GeraetID int16 `json:"geraet_id"` + CheckInOut int16 `json:"check_in_out"` + Timestamp time.Time `json:"timestamp"` + CounterId int `json:"counter_id"` + BookingType BookingType `json:"booking_type"` } type IDatabase interface { @@ -37,31 +38,52 @@ type IDatabase interface { var DB IDatabase -func (b *Booking) New(card_uid string, geraet_id int16, check_in_out int16) Booking { +func (b *Booking) New(cardUid string, gereatId int16, checkInOut int16, typeId int8) Booking { + bookingType, err := GetBookingTypeById(typeId) + if err != nil { + log.Printf("Cannot get booking type %d, from database!", typeId) + } return Booking{ - CardUID: card_uid, - GeraetID: geraet_id, - CheckInOut: check_in_out, + CardUID: cardUid, + GeraetID: gereatId, + CheckInOut: checkInOut, + BookingType: bookingType, } } func (b *Booking) FromUrlParams(params url.Values) Booking { var booking Booking + if _check_in_out, err := strconv.Atoi(params.Get("check_in_out")); err == nil { booking.CheckInOut = int16(_check_in_out) } if _geraet_id, err := strconv.Atoi(params.Get("geraet_id")); err == nil { booking.GeraetID = int16(_geraet_id) } + if _booking_type, err := strconv.Atoi(params.Get("booking_type")); err == nil { + booking.BookingType.Id = int8(_booking_type) + } booking.CardUID = params.Get("card_uid") + return booking } func (b *Booking) Verify() bool { //check for overlapping time + arbeitszeit verstoß if b.CardUID == "" { //|| b.GeraetID == 0 || b.CheckInOut == 0 { + log.Println("Booking verify failed invalid CardUID!") return false } + if b.CheckInOut == 0 { + log.Println("Booking verify failed invalid CheckInOut!") + return false + } + if bookingType, err := GetBookingTypeById(b.BookingType.Id); err != nil { + log.Println("Booking verify failed invalid BookingType.Id!") + return false + } else { + b.BookingType.Name = bookingType.Name + } return true } @@ -69,11 +91,11 @@ func (b *Booking) Insert() error { if !checkLastBooking(*b) { return SameBookingError{} } - stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out) VALUES ($1, $2, $3) RETURNING counter_id, timestamp`)) + stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, anwesenheit_typ) VALUES ($1, $2, $3, $4) RETURNING counter_id, timestamp`)) if err != nil { return err } - err = stmt.QueryRow(b.CardUID, b.GeraetID, b.CheckInOut).Scan(&b.CounterId, &b.Timestamp) + err = stmt.QueryRow(b.CardUID, b.GeraetID, b.CheckInOut, b.BookingType.Id).Scan(&b.CounterId, &b.Timestamp) if err != nil { return err } @@ -84,11 +106,11 @@ func (b *Booking) InsertWithTimestamp() error { if b.Timestamp.IsZero() { return b.Insert() } - stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, timestamp) VALUES ($1, $2, $3, $4) RETURNING counter_id`)) + stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, anwesenheit_typ, timestamp) VALUES ($1, $2, $3, $4, $5) RETURNING counter_id`)) if err != nil { return err } - err = stmt.QueryRow(b.CardUID, b.GeraetID, b.CheckInOut, b.Timestamp).Scan(&b.CounterId) + err = stmt.QueryRow(b.CardUID, b.GeraetID, b.CheckInOut, b.BookingType.Id, b.Timestamp).Scan(&b.CounterId) if err != nil { return err } @@ -97,18 +119,15 @@ func (b *Booking) InsertWithTimestamp() error { func (b *Booking) GetBookingById(booking_id int) (Booking, error) { var booking Booking - qStr, err := DB.Prepare((`SELECT counter_id, timestamp, card_uid, geraet_id, check_in_out FROM anwesenheit WHERE counter_id = $1`)) + qStr, err := DB.Prepare((`SELECT counter_id, timestamp, card_uid, geraet_id, check_in_out, anwesenheit_typ FROM anwesenheit WHERE counter_id = $1`)) if err != nil { return booking, err } - err = qStr.QueryRow(booking_id).Scan(&booking.CounterId, &booking.Timestamp, &booking.CardUID, &booking.GeraetID, &booking.CheckInOut) + // TODO: also get booking type name + err = qStr.QueryRow(booking_id).Scan(&booking.CounterId, &booking.Timestamp, &booking.CardUID, &booking.GeraetID, &booking.CheckInOut, &booking.BookingType.Id) if err != nil { return booking, err } - // if !booking.Verify() { - // fmt.Printf("Booking verification failed! %d", ) - // return booking, nil - // } return booking, nil } @@ -264,6 +283,7 @@ func (b *Booking) UpdateTime(newTime time.Time) { } log.Println("Updating") b.Update(newBooking) + // TODO Check verify b.Verify() b.Save() } @@ -272,7 +292,7 @@ func (b *Booking) ToString() string { return fmt.Sprintf("Booking %d: at: %s, as type: %d", b.CounterId, b.Timestamp.Format("15:04"), b.CheckInOut) } -func GetBokkingTypes() ([]BookingType, error) { +func GetBookingTypes() ([]BookingType, error) { var types []BookingType qStr, err := DB.Prepare("SELECT anwesenheit_id, anwesenheit_name FROM s_anwesenheit_typen;") if err != nil { @@ -295,6 +315,21 @@ func GetBokkingTypes() ([]BookingType, error) { return types, nil } +func GetBookingTypeById(bookingTypeId int8) (BookingType, error) { + var bookingType BookingType = BookingType{Id: bookingTypeId} + + qStr, err := DB.Prepare("SELECT anwesenheit_name FROM s_anwesenheit_typen WHERE anwesenheit_id = $1;") + if err != nil { + return bookingType, err + } + defer qStr.Close() + err = qStr.QueryRow(bookingTypeId).Scan(&bookingType.Name) + if err != nil { + return bookingType, err + } + return bookingType, nil +} + func GetBookingTypesCached() []BookingType { types, err := definedTypes.Get("s_anwesenheit_typen") if err != nil { diff --git a/Backend/models/controlTables.go b/Backend/models/database.go similarity index 90% rename from Backend/models/controlTables.go rename to Backend/models/database.go index e943837..c114c4a 100644 --- a/Backend/models/controlTables.go +++ b/Backend/models/database.go @@ -9,7 +9,7 @@ var definedTypes = helper.NewCache(3600, func(key string) (any, error) { case "s_abwesenheit_typen": return GetAbsenceTypes() case "s_anwesenheit_typen": - return GetBokkingTypes() + return GetBookingTypes() } return nil, nil }) diff --git a/Backend/models/user.go b/Backend/models/user.go index 5bd9ded..4ba7e1d 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -86,10 +86,10 @@ func (u *User) CheckAnwesenheit() bool { // Creates a new booking for the user -> check_in_out will be 254 for automatic check out func (u *User) CheckOut() error { - booking := (*Booking).New(nil, u.CardUID, 0, 254) + booking := (*Booking).New(nil, u.CardUID, 0, 254, 1) err := booking.Insert() if err != nil { - fmt.Printf("Error inserting booking %v\n", err) + fmt.Printf("Error inserting booking %v -> %v\n", booking, err) return err } return nil diff --git a/DB/initdb/01_schema.sql b/DB/initdb/01_schema.sql index 532be9b..c1f4174 100644 --- a/DB/initdb/01_schema.sql +++ b/DB/initdb/01_schema.sql @@ -8,7 +8,7 @@ CREATE TABLE "anwesenheit" ( "card_uid" character varying(255) NOT NULL, "check_in_out" smallint NOT NULL, "geraet_id" smallint NULL, - -- "anwesenheit_typ" int2, + "anwesenheit_typ" int2, PRIMARY KEY ("counter_id") ); diff --git a/Docker/docker-compose.dev.yml b/Docker/docker-compose.dev.yml index b1884d8..5498341 100644 --- a/Docker/docker-compose.dev.yml +++ b/Docker/docker-compose.dev.yml @@ -9,7 +9,7 @@ services: PGDATA: /var/lib/postgresql/data/pg_data volumes: - ${POSTGRES_PATH}:/var/lib/postgresql/data - - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d + # - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d ports: - 5432:5432 diff --git a/db.sql b/db.sql index 0e59269..8f125b8 100644 --- a/db.sql +++ b/db.sql @@ -146,3 +146,51 @@ SELECT FROM ordered_bookings GROUP BY work_date ORDER BY work_date; + + +-- Generate weekdays for 2 weeks (Mon–Fri), starting 2 weeks ago +WITH days AS ( + SELECT gs::date AS work_date + FROM generate_series( + date_trunc('week', CURRENT_DATE) - interval '14 days', -- start 2 weeks ago Monday + CURRENT_DATE, -- end TODAY (no future days) + interval '1 day' + ) gs + WHERE EXTRACT(ISODOW FROM gs) <= 5 -- only Mon–Fri +), +sample_bookings AS ( + SELECT + d.work_date, + 'aaaa-aaaa'::varchar AS card_uid, + 1 AS check_in_out, -- come + 101 AS geraet_id, + (d.work_date + make_time(8, floor(random()*50)::int, 0))::timestamptz AS ts, + 1 AS anwesenheit_typ + FROM days d + UNION ALL + SELECT + d.work_date, + 'aaaa-aaaa'::varchar AS card_uid, + 2 AS check_in_out, -- go + 101 AS geraet_id, + (d.work_date + make_time(16, floor(random()*50)::int, 0))::timestamptz AS ts, + 1 AS anwesenheit_typ + FROM days d +), +ins_anw AS ( + -- insert only bookings up to now (prevents future times on today) + INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id) + SELECT ts, card_uid, check_in_out, geraet_id + FROM sample_bookings + WHERE ts <= NOW() + RETURNING 1 +) +-- now insert absences (uses the same days CTE) +INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum) +SELECT + 'aaaa-aaaa', + (ARRAY[1, 2])[floor(random()*2 + 1)], -- example types + d.work_date::timestamptz +FROM days d +WHERE random() < 0.2 -- ~20% random absences +ORDER BY d.work_date; diff --git a/migrations/20250901201159_initial.down.sql b/migrations/20250901201159_initial.down.sql new file mode 100644 index 0000000..1f8630e --- /dev/null +++ b/migrations/20250901201159_initial.down.sql @@ -0,0 +1,16 @@ +-- reverse: create "wochen_report" table +DROP TABLE "wochen_report"; +-- reverse: create "user_password" table +DROP TABLE "user_password"; +-- reverse: set comment to column: "geschlecht" on table: "personal_daten" +COMMENT ON COLUMN "personal_daten"."geschlecht" IS NULL; +-- reverse: create "personal_daten" table +DROP TABLE "personal_daten"; +-- reverse: set comment to column: "geraet_id" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."geraet_id" IS NULL; +-- reverse: set comment to column: "check_in_out" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."check_in_out" IS NULL; +-- reverse: create "anwesenheit" table +DROP TABLE "anwesenheit"; +-- reverse: create "abwesenheit" table +DROP TABLE "abwesenheit"; diff --git a/migrations/20250901201159_initial.up.sql b/migrations/20250901201159_initial.up.sql new file mode 100644 index 0000000..5144756 --- /dev/null +++ b/migrations/20250901201159_initial.up.sql @@ -0,0 +1,57 @@ +-- create "abwesenheit" table +CREATE TABLE "abwesenheit" ( + "counter_id" bigserial NOT NULL, + "card_uid" character varying(255) NULL, + "abwesenheit_typ" smallint NULL, + "datum" timestamptz NULL DEFAULT (now())::date, + PRIMARY KEY ("counter_id") +); +-- create "anwesenheit" table +CREATE TABLE "anwesenheit" ( + "counter_id" bigserial NOT NULL, + "timestamp" timestamptz NULL DEFAULT CURRENT_TIMESTAMP, + "card_uid" character varying(255) NULL, + "check_in_out" smallint NULL, + "geraet_id" smallint NULL, + PRIMARY KEY ("counter_id") +); +-- set comment to column: "check_in_out" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."check_in_out" IS '1=Check In 2=Check Out , 3=Check in Manuell, 4=Check out manuell255=Automatic Check Out'; +-- set comment to column: "geraet_id" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."geraet_id" IS 'ID des Lesegerätes'; +-- create "personal_daten" table +CREATE TABLE "personal_daten" ( + "personal_nummer" integer NOT NULL, + "aktiv_beschaeftigt" boolean NULL, + "vorname" character varying(255) NULL, + "nachname" character varying(255) NULL, + "geburtsdatum" date NULL, + "plz" character varying(255) NULL, + "adresse" character varying(255) NULL, + "geschlecht" smallint NULL, + "card_uid" character varying(255) NULL, + "hauptbeschaeftigungs_ort" smallint NULL, + "arbeitszeit_per_tag" real NULL, + "arbeitszeit_min_start" time NULL, + "arbeitszeit_max_ende" time NULL, + "vorgesetzter_pers_nr" integer NULL, + PRIMARY KEY ("personal_nummer") +); +-- set comment to column: "geschlecht" on table: "personal_daten" +COMMENT ON COLUMN "personal_daten"."geschlecht" IS '1==weiblich, 2==maennlich, 3==divers'; +-- create "user_password" table +CREATE TABLE "user_password" ( + "personal_nummer" integer NOT NULL, + "pass_hash" text NULL, + "zuletzt_geandert" timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("personal_nummer") +); +-- create "wochen_report" table +CREATE TABLE "wochen_report" ( + "id" serial NOT NULL, + "personal_nummer" integer NULL, + "woche_start" date NULL, + "bestaetigt" boolean NULL DEFAULT false, + PRIMARY KEY ("id"), + CONSTRAINT "wochen_report_personal_nummer_woche_start_key" UNIQUE ("personal_nummer", "woche_start") +); diff --git a/migrations/20250901201250_control_tables.down.sql b/migrations/20250901201250_control_tables.down.sql new file mode 100644 index 0000000..b59cc2d --- /dev/null +++ b/migrations/20250901201250_control_tables.down.sql @@ -0,0 +1,31 @@ +-- reverse: drop "personal_daten" table +CREATE TABLE "personal_daten" ( + "personal_nummer" integer NOT NULL, + "aktiv_beschaeftigt" boolean NULL, + "vorname" character varying(255) NULL, + "nachname" character varying(255) NULL, + "geburtsdatum" date NULL, + "plz" character varying(255) NULL, + "adresse" character varying(255) NULL, + "geschlecht" smallint NULL, + "card_uid" character varying(255) NULL, + "hauptbeschaeftigungs_ort" smallint NULL, + "arbeitszeit_per_tag" real NULL, + "arbeitszeit_min_start" time NULL, + "arbeitszeit_max_ende" time NULL, + "vorgesetzter_pers_nr" integer NULL, + PRIMARY KEY ("personal_nummer") +); +COMMENT ON COLUMN "personal_daten"."geschlecht" IS '1==weiblich, 2==maennlich, 3==divers'; +-- reverse: set comment to column: "geschlecht" on table: "s_personal_daten" +COMMENT ON COLUMN "s_personal_daten"."geschlecht" IS NULL; +-- reverse: create "s_personal_daten" table +DROP TABLE "s_personal_daten"; +-- reverse: create "s_anwesenheit_typen" table +DROP TABLE "s_anwesenheit_typen"; +-- reverse: create "s_abwesenheit_typen" table +DROP TABLE "s_abwesenheit_typen"; +-- reverse: modify "wochen_report" table +ALTER TABLE "wochen_report" DROP COLUMN "ueberstunden"; +-- reverse: modify "anwesenheit" table +ALTER TABLE "anwesenheit" DROP COLUMN "anwesenheit_typ", ALTER COLUMN "check_in_out" DROP NOT NULL, ALTER COLUMN "card_uid" DROP NOT NULL; diff --git a/migrations/20250901201250_control_tables.up.sql b/migrations/20250901201250_control_tables.up.sql new file mode 100644 index 0000000..53fa8d8 --- /dev/null +++ b/migrations/20250901201250_control_tables.up.sql @@ -0,0 +1,38 @@ +-- modify "anwesenheit" table +ALTER TABLE "anwesenheit" ALTER COLUMN "card_uid" SET NOT NULL, ALTER COLUMN "check_in_out" SET NOT NULL, ADD COLUMN "anwesenheit_typ" smallint NULL; +-- modify "wochen_report" table +ALTER TABLE "wochen_report" ADD COLUMN "ueberstunden" smallint NULL; +-- create "s_abwesenheit_typen" table +CREATE TABLE "s_abwesenheit_typen" ( + "abwesenheit_id" smallint NOT NULL, + "abwesenheit_name" character varying(255) NULL, + PRIMARY KEY ("abwesenheit_id") +); +-- create "s_anwesenheit_typen" table +CREATE TABLE "s_anwesenheit_typen" ( + "anwesenheit_id" smallint NOT NULL, + "anwesenheit_name" character varying(255) NULL, + PRIMARY KEY ("anwesenheit_id") +); +-- create "s_personal_daten" table +CREATE TABLE "s_personal_daten" ( + "personal_nummer" integer NOT NULL, + "aktiv_beschaeftigt" boolean NULL, + "vorname" character varying(255) NULL, + "nachname" character varying(255) NULL, + "geburtsdatum" date NULL, + "plz" character varying(255) NULL, + "adresse" character varying(255) NULL, + "geschlecht" smallint NULL, + "card_uid" character varying(255) NULL, + "hauptbeschaeftigungs_ort" smallint NULL, + "arbeitszeit_per_tag" real NULL, + "arbeitszeit_min_start" time NULL, + "arbeitszeit_max_ende" time NULL, + "vorgesetzter_pers_nr" integer NULL, + PRIMARY KEY ("personal_nummer") +); +-- set comment to column: "geschlecht" on table: "s_personal_daten" +COMMENT ON COLUMN "s_personal_daten"."geschlecht" IS '1==weiblich, 2==maennlich, 3==divers'; +-- drop "personal_daten" table +DROP TABLE "personal_daten"; diff --git a/migrations/20250901201710_triggers_extension.sql b/migrations/20250901201710_triggers_extension.sql new file mode 100644 index 0000000..5e8ad1f --- /dev/null +++ b/migrations/20250901201710_triggers_extension.sql @@ -0,0 +1,21 @@ +-- update Funktion für pass_hash + +CREATE OR REPLACE FUNCTION update_zuletzt_geandert() +RETURNS TRIGGER AS $$ +BEGIN + -- Nur wenn hash geändert wurde + IF NEW.pass_hash IS DISTINCT FROM OLD.pass_hash THEN + NEW.zuletzt_geandert = now(); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER pass_hash_update +BEFORE UPDATE ON user_password +FOR EACH ROW +EXECUTE FUNCTION update_zuletzt_geandert(); + +-- Adds crypto extension + +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/migrations/atlas.sum b/migrations/atlas.sum new file mode 100644 index 0000000..306e8b4 --- /dev/null +++ b/migrations/atlas.sum @@ -0,0 +1,6 @@ +h1:M1O+1WNf/zb6bwiQPExxUhhXL9S4TtZ4qAsuRr0/Zq4= +20250901201159_initial.down.sql h1:BkpujZk5zDCVVoroqrZlXgVR0nvT5Sbzye6aR5e6Z5w= +20250901201159_initial.up.sql h1:SAruU753YcQ0oFa3Ii6ylzesLulAKD1j74zDvqv3BDQ= +20250901201250_control_tables.down.sql h1:4jA+wm0/Ag86KdkKPZfnADsAlOQl1FYIDX8pdfsSYlA= +20250901201250_control_tables.up.sql h1:IGDQ9nT39D12buAi0SUauygXH2ZrCh/YNsZGtk9ztWc= +20250901201710_triggers_extension.sql h1:2Oki9mr3nJBE/supbY9HIr+wp4XJT76a38JTByjnHf0=