Compare · 031

PostgreSQL power queries: TimescaleDB and PostgreSQL

TimescaleDB and PostgreSQL run the same files for this example: the SQL and the Go are identical.

Compare TimescaleDB withPostgreSQL

Configuration

sgl.yamlidentical
TimescaleDB
PostgreSQL
1version: 1
1version: 1
2
2
3databases:
3databases:
4 - name: harbour
4 - name: harbour
5 dialect: postgres
5 dialect: postgres
6 migrations: db/migrations
6 migrations: db/migrations
7 queries: db/queries
7 queries: db/queries
8 go:
8 go:
9 package: store
9 package: store
10 out: internal/store
10 out: internal/store
11 driver: database/sql
11 driver: database/sql
12 migrate:
12 migrate:
13 dsn: ${env:DATABASE_URL}
13 dsn: ${env:DATABASE_URL}

Migrations

db/migrations/0001_harbour.up.sqlidentical
TimescaleDB
PostgreSQL
1CREATE TABLE boats (
1CREATE TABLE boats (
2 id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
2 id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
3 name TEXT NOT NULL,
3 name TEXT NOT NULL,
4 gear JSONB NOT NULL DEFAULT '{}'
4 gear JSONB NOT NULL DEFAULT '{}'
5);
5);
6
6
7CREATE TABLE landings (
7CREATE TABLE landings (
8 id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
8 id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
9 boat_id BIGINT NOT NULL REFERENCES boats (id),
9 boat_id BIGINT NOT NULL REFERENCES boats (id),
10 species TEXT NOT NULL,
10 species TEXT NOT NULL,
11 kilos INTEGER NOT NULL,
11 kilos INTEGER NOT NULL,
12 landed_on DATE NOT NULL,
12 landed_on DATE NOT NULL,
13 notes TEXT NOT NULL,
13 notes TEXT NOT NULL,
14 search TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', species || ' ' || notes)) STORED
14 search TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', species || ' ' || notes)) STORED
15);
15);
16
16
17CREATE INDEX landings_search_idx ON landings USING GIN (search);
17CREATE INDEX landings_search_idx ON landings USING GIN (search);
18
18
19CREATE TABLE landing_archive (
19CREATE TABLE landing_archive (
20 id BIGINT PRIMARY KEY,
20 id BIGINT PRIMARY KEY,
21 boat_id BIGINT NOT NULL REFERENCES boats (id),
21 boat_id BIGINT NOT NULL REFERENCES boats (id),
22 species TEXT NOT NULL,
22 species TEXT NOT NULL,
23 kilos INTEGER NOT NULL,
23 kilos INTEGER NOT NULL,
24 landed_on DATE NOT NULL
24 landed_on DATE NOT NULL
25);
25);
db/migrations/0001_harbour.down.sqlidentical
TimescaleDB
PostgreSQL
1DROP TABLE landing_archive;
1DROP TABLE landing_archive;
2DROP TABLE landings;
2DROP TABLE landings;
3DROP TABLE boats;
3DROP TABLE boats;

Queries

AddBoatidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1-- sgl.query(AddBoat, one)
1-- sgl.query(AddBoat, one)
2INSERT INTO boats (name, gear) VALUES ($1, $2) RETURNING id;
2INSERT INTO boats (name, gear) VALUES ($1, $2) RETURNING id;
RecordLandingidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1-- sgl.query(RecordLanding, exec)
1-- sgl.query(RecordLanding, exec)
2INSERT INTO landings (boat_id, species, kilos, landed_on, notes) VALUES ($1, $2, $3, $4, $5);
2INSERT INTO landings (boat_id, species, kilos, landed_on, notes) VALUES ($1, $2, $3, $4, $5);
LatestLandingsidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1-- sgl.query(LatestLandings, many)
1-- sgl.query(LatestLandings, many)
2SELECT DISTINCT ON (landings.boat_id) boats.name, landings.species, landings.landed_on
2SELECT DISTINCT ON (landings.boat_id) boats.name, landings.species, landings.landed_on
3FROM landings
3FROM landings
4JOIN boats ON boats.id = landings.boat_id
4JOIN boats ON boats.id = landings.boat_id
5ORDER BY landings.boat_id, landings.landed_on DESC, landings.id DESC;
5ORDER BY landings.boat_id, landings.landed_on DESC, landings.id DESC;
BiggestLandingsidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1-- sgl.query(BiggestLandings, many)
1-- sgl.query(BiggestLandings, many)
2SELECT boats.name, biggest.species, biggest.kilos
2SELECT boats.name, biggest.species, biggest.kilos
3FROM boats
3FROM boats
4LEFT JOIN LATERAL (
4LEFT JOIN LATERAL (
5 SELECT species, kilos
5 SELECT species, kilos
6 FROM landings
6 FROM landings
7 WHERE landings.boat_id = boats.id
7 WHERE landings.boat_id = boats.id
8 ORDER BY kilos DESC
8 ORDER BY kilos DESC
9 LIMIT 2
9 LIMIT 2
10) AS biggest ON true
10) AS biggest ON true
11ORDER BY boats.name, biggest.kilos DESC;
11ORDER BY boats.name, biggest.kilos DESC;
SearchNotesidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1-- sgl.query(SearchNotes, many)
1-- sgl.query(SearchNotes, many)
2SELECT landings.id, landings.notes, ts_rank(landings.search, websearch_to_tsquery('english', :terms)) AS rank
2SELECT landings.id, landings.notes, ts_rank(landings.search, websearch_to_tsquery('english', :terms)) AS rank
3FROM landings
3FROM landings
4WHERE landings.search @@ websearch_to_tsquery('english', :terms)
4WHERE landings.search @@ websearch_to_tsquery('english', :terms)
5ORDER BY rank DESC, landings.id;
5ORDER BY rank DESC, landings.id;
BoatGearidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1-- sgl.query(BoatGear, many)
1-- sgl.query(BoatGear, many)
2SELECT boats.name, gear.key, gear.value
2SELECT boats.name, gear.key, gear.value
3FROM boats
3FROM boats
4CROSS JOIN LATERAL jsonb_each(boats.gear) AS gear
4CROSS JOIN LATERAL jsonb_each(boats.gear) AS gear
5ORDER BY boats.name, gear.key;
5ORDER BY boats.name, gear.key;
ArchiveLandingsidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1-- sgl.query(ArchiveLandings, many)
1-- sgl.query(ArchiveLandings, many)
2WITH archived AS (
2WITH archived AS (
3 DELETE FROM landings
3 DELETE FROM landings
4 WHERE landed_on < :before
4 WHERE landed_on < :before
5 RETURNING id, boat_id, species, kilos, landed_on
5 RETURNING id, boat_id, species, kilos, landed_on
6)
6)
7INSERT INTO landing_archive (id, boat_id, species, kilos, landed_on)
7INSERT INTO landing_archive (id, boat_id, species, kilos, landed_on)
8SELECT id, boat_id, species, kilos, landed_on FROM archived
8SELECT id, boat_id, species, kilos, landed_on FROM archived
9RETURNING id, species, landed_on;
9RETURNING id, species, landed_on;

What sgl generates

AddBoatidentical
TimescaleDB
PostgreSQL
1func (queries *Queries) AddBoat(ctx context.Context, params AddBoatParams) (AddBoatRow, error)
1func (queries *Queries) AddBoat(ctx context.Context, params AddBoatParams) (AddBoatRow, error)
2
2
3type AddBoatParams struct {
3type AddBoatParams struct {
4 Name string
4 Name string
5 Gear JSON
5 Gear JSON
6}
6}
7
7
8type AddBoatRow struct {
8type AddBoatRow struct {
9 ID int64 `json:"id"`
9 ID int64 `json:"id"`
10}
10}
11
11
12type JSON []byte
12type JSON []byte
RecordLandingidentical
TimescaleDB
PostgreSQL
1func (queries *Queries) RecordLanding(ctx context.Context, params RecordLandingParams) error
1func (queries *Queries) RecordLanding(ctx context.Context, params RecordLandingParams) error
2
2
3type RecordLandingParams struct {
3type RecordLandingParams struct {
4 BoatID int64
4 BoatID int64
5 Species string
5 Species string
6 Kilos int32
6 Kilos int32
7 LandedOn time.Time
7 LandedOn time.Time
8 Notes string
8 Notes string
9}
9}
LatestLandingsidentical
TimescaleDB
PostgreSQL
1func (queries *Queries) LatestLandings(ctx context.Context) ([]LatestLandingsRow, error)
1func (queries *Queries) LatestLandings(ctx context.Context) ([]LatestLandingsRow, error)
2
2
3type LatestLandingsRow struct {
3type LatestLandingsRow struct {
4 Name string `json:"name"`
4 Name string `json:"name"`
5 Species string `json:"species"`
5 Species string `json:"species"`
6 LandedOn time.Time `json:"landed_on"`
6 LandedOn time.Time `json:"landed_on"`
7}
7}
BiggestLandingsidentical
TimescaleDB
PostgreSQL
1func (queries *Queries) BiggestLandings(ctx context.Context) ([]BiggestLandingsRow, error)
1func (queries *Queries) BiggestLandings(ctx context.Context) ([]BiggestLandingsRow, error)
2
2
3type BiggestLandingsRow struct {
3type BiggestLandingsRow struct {
4 Name string `json:"name"`
4 Name string `json:"name"`
5 Species *string `json:"species"`
5 Species *string `json:"species"`
6 Kilos *int32 `json:"kilos"`
6 Kilos *int32 `json:"kilos"`
7}
7}
SearchNotesidentical
TimescaleDB
PostgreSQL
1func (queries *Queries) SearchNotes(ctx context.Context, terms string) ([]SearchNotesRow, error)
1func (queries *Queries) SearchNotes(ctx context.Context, terms string) ([]SearchNotesRow, error)
2
2
3type SearchNotesRow struct {
3type SearchNotesRow struct {
4 ID int64 `json:"id"`
4 ID int64 `json:"id"`
5 Notes string `json:"notes"`
5 Notes string `json:"notes"`
6 Rank *float32 `json:"rank"`
6 Rank *float32 `json:"rank"`
7}
7}
BoatGearidentical
TimescaleDB
PostgreSQL
1func (queries *Queries) BoatGear(ctx context.Context) ([]BoatGearRow, error)
1func (queries *Queries) BoatGear(ctx context.Context) ([]BoatGearRow, error)
2
2
3type BoatGearRow struct {
3type BoatGearRow struct {
4 Name string `json:"name"`
4 Name string `json:"name"`
5 Key string `json:"key"`
5 Key string `json:"key"`
6 Value JSON `json:"value"`
6 Value JSON `json:"value"`
7}
7}
8
8
9type JSON []byte
9type JSON []byte
ArchiveLandingsidentical
TimescaleDB
PostgreSQL
1func (queries *Queries) ArchiveLandings(ctx context.Context, before time.Time) ([]ArchiveLandingsRow, error)
1func (queries *Queries) ArchiveLandings(ctx context.Context, before time.Time) ([]ArchiveLandingsRow, error)
2
2
3type ArchiveLandingsRow struct {
3type ArchiveLandingsRow struct {
4 ID int64 `json:"id"`
4 ID int64 `json:"id"`
5 Species string `json:"species"`
5 Species string `json:"species"`
6 LandedOn time.Time `json:"landed_on"`
6 LandedOn time.Time `json:"landed_on"`
7}
7}

The program

example.goidentical
TimescaleDB
PostgreSQL
1package main
1package main
2
2
3import (
3import (
4 "cmp"
4 "cmp"
5 "context"
5 "context"
6 "database/sql"
6 "database/sql"
7 "fmt"
7 "fmt"
8 "io"
8 "io"
9 "slices"
9 "slices"
10 "time"
10 "time"
11
11
12 "seagull.sh/seagull/examples/postgres/031_postgres_power_queries/internal/store"
12 "seagull.sh/seagull/examples/postgres/031_postgres_power_queries/internal/store"
13)
13)
14
14
15// run records a week of landings at a small harbour and reads them back with five queries
15// run records a week of landings at a small harbour and reads them back with five queries
16// that lean on PostgreSQL's own SQL. Seagull types each of them:
16// that lean on PostgreSQL's own SQL. Seagull types each of them:
17//
17//
18// - LatestLandings uses DISTINCT ON to keep the first row of each boat in ORDER BY
18// - LatestLandings uses DISTINCT ON to keep the first row of each boat in ORDER BY
19// order, which is its latest landing;
19// order, which is its latest landing;
20// - BiggestLandings runs a LIMIT 2 subquery once per boat with LEFT JOIN LATERAL, so
20// - BiggestLandings runs a LIMIT 2 subquery once per boat with LEFT JOIN LATERAL, so
21// the subquery can see boats.id; a boat with no landings still appears, which makes
21// the subquery can see boats.id; a boat with no landings still appears, which makes
22// the subquery's columns pointers;
22// the subquery's columns pointers;
23// - SearchNotes matches a generated tsvector column against websearch_to_tsquery, which
23// - SearchNotes matches a generated tsvector column against websearch_to_tsquery, which
24// takes the syntax people type into search boxes, and ranks with ts_rank;
24// takes the syntax people type into search boxes, and ranks with ts_rank;
25// - BoatGear expands each boat's JSONB gear into key and value rows with jsonb_each, a
25// - BoatGear expands each boat's JSONB gear into key and value rows with jsonb_each, a
26// function used as a table; value stays store.JSON;
26// function used as a table; value stays store.JSON;
27// - ArchiveLandings moves old landings in one statement: the DELETE inside the CTE
27// - ArchiveLandings moves old landings in one statement: the DELETE inside the CTE
28// hands its RETURNING rows to the INSERT, and the INSERT's RETURNING comes back.
28// hands its RETURNING rows to the INSERT, and the INSERT's RETURNING comes back.
29//
29//
30// Takes database (*sql.DB) which is a migrated database.
30// Takes database (*sql.DB) which is a migrated database.
31// Takes output (io.Writer) which receives each query's result.
31// Takes output (io.Writer) which receives each query's result.
32//
32//
33// Returns error when a query fails.
33// Returns error when a query fails.
34func run(ctx context.Context, database *sql.DB, output io.Writer) error {
34func run(ctx context.Context, database *sql.DB, output io.Writer) error {
35 queries := store.New(database)
35 queries := store.New(database)
36
36
37 if err := recordWeek(ctx, queries); err != nil {
37 if err := recordWeek(ctx, queries); err != nil {
38 return err
38 return err
39 }
39 }
40
40
41 latest, err := queries.LatestLandings(ctx)
41 latest, err := queries.LatestLandings(ctx)
42 if err != nil {
42 if err != nil {
43 return fmt.Errorf("finding the latest landings: %w", err)
43 return fmt.Errorf("finding the latest landings: %w", err)
44 }
44 }
45 fmt.Fprintln(output, "latest landing per boat (DISTINCT ON):")
45 fmt.Fprintln(output, "latest landing per boat (DISTINCT ON):")
46 for _, landing := range latest {
46 for _, landing := range latest {
47 fmt.Fprintf(output, " %-9s %-8s %s\n", landing.Name, landing.Species, landing.LandedOn.Format(time.DateOnly))
47 fmt.Fprintf(output, " %-9s %-8s %s\n", landing.Name, landing.Species, landing.LandedOn.Format(time.DateOnly))
48 }
48 }
49
49
50 biggest, err := queries.BiggestLandings(ctx)
50 biggest, err := queries.BiggestLandings(ctx)
51 if err != nil {
51 if err != nil {
52 return fmt.Errorf("finding the biggest landings: %w", err)
52 return fmt.Errorf("finding the biggest landings: %w", err)
53 }
53 }
54 fmt.Fprintln(output, "two biggest landings per boat (LEFT JOIN LATERAL):")
54 fmt.Fprintln(output, "two biggest landings per boat (LEFT JOIN LATERAL):")
55 for _, landing := range biggest {
55 for _, landing := range biggest {
56 if landing.Species == nil || landing.Kilos == nil {
56 if landing.Species == nil || landing.Kilos == nil {
57 fmt.Fprintf(output, " %-9s nothing landed\n", landing.Name)
57 fmt.Fprintf(output, " %-9s nothing landed\n", landing.Name)
58 continue
58 continue
59 }
59 }
60 fmt.Fprintf(output, " %-9s %-8s %3dkg\n", landing.Name, *landing.Species, *landing.Kilos)
60 fmt.Fprintf(output, " %-9s %-8s %3dkg\n", landing.Name, *landing.Species, *landing.Kilos)
61 }
61 }
62
62
63 for _, terms := range []string{"mackerel", "crab or pollock"} {
63 for _, terms := range []string{"mackerel", "crab or pollock"} {
64 found, err := queries.SearchNotes(ctx, terms)
64 found, err := queries.SearchNotes(ctx, terms)
65 if err != nil {
65 if err != nil {
66 return fmt.Errorf("searching for %q: %w", terms, err)
66 return fmt.Errorf("searching for %q: %w", terms, err)
67 }
67 }
68 fmt.Fprintf(output, "search %q (full-text):\n", terms)
68 fmt.Fprintf(output, "search %q (full-text):\n", terms)
69 for _, match := range found {
69 for _, match := range found {
70 fmt.Fprintf(output, " #%d rank %s %s\n", match.ID, formatRank(match.Rank), match.Notes)
70 fmt.Fprintf(output, " #%d rank %s %s\n", match.ID, formatRank(match.Rank), match.Notes)
71 }
71 }
72 }
72 }
73
73
74 gear, err := queries.BoatGear(ctx)
74 gear, err := queries.BoatGear(ctx)
75 if err != nil {
75 if err != nil {
76 return fmt.Errorf("listing gear: %w", err)
76 return fmt.Errorf("listing gear: %w", err)
77 }
77 }
78 fmt.Fprintln(output, "gear (jsonb_each):")
78 fmt.Fprintln(output, "gear (jsonb_each):")
79 for _, item := range gear {
79 for _, item := range gear {
80 fmt.Fprintf(output, " %-9s %-5s %s\n", item.Name, item.Key, item.Value)
80 fmt.Fprintf(output, " %-9s %-5s %s\n", item.Name, item.Key, item.Value)
81 }
81 }
82
82
83 cutoff := july(3)
83 cutoff := july(3)
84 archived, err := queries.ArchiveLandings(ctx, cutoff)
84 archived, err := queries.ArchiveLandings(ctx, cutoff)
85 if err != nil {
85 if err != nil {
86 return fmt.Errorf("archiving landings: %w", err)
86 return fmt.Errorf("archiving landings: %w", err)
87 }
87 }
88 // RETURNING rows come back in no promised order, so sort them.
88 // RETURNING rows come back in no promised order, so sort them.
89 slices.SortFunc(archived, func(a, b store.ArchiveLandingsRow) int { return cmp.Compare(a.ID, b.ID) })
89 slices.SortFunc(archived, func(a, b store.ArchiveLandingsRow) int { return cmp.Compare(a.ID, b.ID) })
90 fmt.Fprintf(output, "archived before %s (DELETE and INSERT in one statement):\n", cutoff.Format(time.DateOnly))
90 fmt.Fprintf(output, "archived before %s (DELETE and INSERT in one statement):\n", cutoff.Format(time.DateOnly))
91 for _, landing := range archived {
91 for _, landing := range archived {
92 fmt.Fprintf(output, " #%d %-8s %s\n", landing.ID, landing.Species, landing.LandedOn.Format(time.DateOnly))
92 fmt.Fprintf(output, " #%d %-8s %s\n", landing.ID, landing.Species, landing.LandedOn.Format(time.DateOnly))
93 }
93 }
94 return nil
94 return nil
95}
95}
96
96
97// recordWeek adds three boats and five landings.
97// recordWeek adds three boats and five landings.
98//
98//
99// Takes queries (*store.Queries) which add the rows.
99// Takes queries (*store.Queries) which add the rows.
100//
100//
101// Returns error when a row cannot be added.
101// Returns error when a row cannot be added.
102func recordWeek(ctx context.Context, queries *store.Queries) error {
102func recordWeek(ctx context.Context, queries *store.Queries) error {
103 boats := []store.AddBoatParams{
103 boats := []store.AddBoatParams{
104 {Name: "Kittiwake", Gear: store.JSON(`{"nets": 2, "pots": 40}`)},
104 {Name: "Kittiwake", Gear: store.JSON(`{"nets": 2, "pots": 40}`)},
105 {Name: "Puffin", Gear: store.JSON(`{"lines": 12}`)},
105 {Name: "Puffin", Gear: store.JSON(`{"lines": 12}`)},
106 {Name: "Gannet", Gear: store.JSON(`{}`)},
106 {Name: "Gannet", Gear: store.JSON(`{}`)},
107 }
107 }
108 ids := make(map[string]int64, len(boats))
108 ids := make(map[string]int64, len(boats))
109 for _, boat := range boats {
109 for _, boat := range boats {
110 added, err := queries.AddBoat(ctx, boat)
110 added, err := queries.AddBoat(ctx, boat)
111 if err != nil {
111 if err != nil {
112 return fmt.Errorf("adding %s: %w", boat.Name, err)
112 return fmt.Errorf("adding %s: %w", boat.Name, err)
113 }
113 }
114 ids[boat.Name] = added.ID
114 ids[boat.Name] = added.ID
115 }
115 }
116 landings := []store.RecordLandingParams{
116 landings := []store.RecordLandingParams{
117 {BoatID: ids["Kittiwake"], Species: "mackerel", Kilos: 120, LandedOn: july(1), Notes: "a big shoal of mackerel off the point"},
117 {BoatID: ids["Kittiwake"], Species: "mackerel", Kilos: 120, LandedOn: july(1), Notes: "a big shoal of mackerel off the point"},
118 {BoatID: ids["Kittiwake"], Species: "crab", Kilos: 35, LandedOn: july(2), Notes: "pots hauled in a heavy swell"},
118 {BoatID: ids["Kittiwake"], Species: "crab", Kilos: 35, LandedOn: july(2), Notes: "pots hauled in a heavy swell"},
119 {BoatID: ids["Puffin"], Species: "pollock", Kilos: 60, LandedOn: july(1), Notes: "lines over the wrecks"},
119 {BoatID: ids["Puffin"], Species: "pollock", Kilos: 60, LandedOn: july(1), Notes: "lines over the wrecks"},
120 {BoatID: ids["Kittiwake"], Species: "mackerel", Kilos: 80, LandedOn: july(4), Notes: "feathers out, gulls everywhere"},
120 {BoatID: ids["Kittiwake"], Species: "mackerel", Kilos: 80, LandedOn: july(4), Notes: "feathers out, gulls everywhere"},
121 {BoatID: ids["Puffin"], Species: "mackerel", Kilos: 25, LandedOn: july(3), Notes: "a few mackerel by the harbour wall"},
121 {BoatID: ids["Puffin"], Species: "mackerel", Kilos: 25, LandedOn: july(3), Notes: "a few mackerel by the harbour wall"},
122 }
122 }
123 for _, landing := range landings {
123 for _, landing := range landings {
124 if err := queries.RecordLanding(ctx, landing); err != nil {
124 if err := queries.RecordLanding(ctx, landing); err != nil {
125 return fmt.Errorf("recording %s: %w", landing.Species, err)
125 return fmt.Errorf("recording %s: %w", landing.Species, err)
126 }
126 }
127 }
127 }
128 return nil
128 return nil
129}
129}
130
130
131// july returns a day in July 2026, the week the landings happen in.
131// july returns a day in July 2026, the week the landings happen in.
132//
132//
133// Takes day (int) which is the day of the month.
133// Takes day (int) which is the day of the month.
134//
134//
135// Returns time.Time which is midnight UTC on that day, as a DATE column holds it.
135// Returns time.Time which is midnight UTC on that day, as a DATE column holds it.
136func july(day int) time.Time {
136func july(day int) time.Time {
137 return time.Date(2026, time.July, day, 0, 0, 0, 0, time.UTC)
137 return time.Date(2026, time.July, day, 0, 0, 0, 0, time.UTC)
138}
138}
139
139
140// formatRank formats a ts_rank score to four places, or returns NULL when it is nil.
140// formatRank formats a ts_rank score to four places, or returns NULL when it is nil.
141//
141//
142// Takes rank (*float32) which is the score.
142// Takes rank (*float32) which is the score.
143//
143//
144// Returns string which is the text to print.
144// Returns string which is the text to print.
145func formatRank(rank *float32) string {
145func formatRank(rank *float32) string {
146 if rank == nil {
146 if rank == nil {
147 return "NULL"
147 return "NULL"
148 }
148 }
149 return fmt.Sprintf("%.4f", *rank)
149 return fmt.Sprintf("%.4f", *rank)
150}
150}