-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
simplified docs and removed redundant texts.
- Loading branch information
1 parent
612a34a
commit 9017e95
Showing
9 changed files
with
303 additions
and
570 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# Database readme | ||
|
||
Previous iterations of the Defacto2 web application relied on MySQL for its database. But for this 2023, Go application rewrite, the site will use [PostgreSQL](https://www.postgresql.org). With the Defacto2 database schema, Postgre uses less memory and is far more performant with complex queries. | ||
|
||
Postgre is more strict about data types than MySQL. For example, inserting a string into a numeric column in Postgre will throw an error, whereas MySQL will convert the string to a number. Postgre has a more powerful query optimizer meaning queries often run faster with complex joins and subqueries. | ||
|
||
# Table and data <small>changes to implement</small> | ||
|
||
These are only suggestions and may not be necessary if they create too much work or complexity. | ||
|
||
- [X] Rename `defacto2-inno` database to `defacto2_ps` | ||
- [ ] Rename `files` table to `release` or `releases` | ||
- [ ] Create a `release_tests` table with a selection of 20 read-only records | ||
- [ ] Rename `files.createdat`, `deleteat`, `updatedat` etc to `x_at` aka `create_at`... | ||
- - [ ] __OR__ break convention and use `date_created`, `date_deleted`, `date_updated` etc. | ||
|
||
### [datatypes](https://www.postgresql.org/docs/current/datatype.html) differences | ||
|
||
- [ ] `CITEXT` type for case-insensitive character strings | ||
- [ ] `files.filesize` should be converted to an `integer`, 4 bytes to permit a 2.147GB value | ||
- [ ] `files.id` should be converted to a `serial` type | ||
- [ ] There is no performance improvement for `fixed-length`, padded character types, etc, meaning strings can use `varchar`(n) or `text`. | ||
|
||
### Indexes | ||
|
||
- [ ] Create PostgreSQL *indexes* with case-sensitive strings for [optimal performance](https://wirekat.com/optimizing-sql-based-on-postgresql/)? | ||
- [ ] Partial Indexes: Use partial indexes when you only need to index a subset of rows, such as, | ||
- - `CREATE INDEX ON orders (order_date) WHERE status = 'SHIPPED'`; | ||
- [ ] Over-Indexing: Creating too many indexes can slow down write operations, as each index needs to be updated on `INSERT`, `UPDATE`, or `DELETE` operations. | ||
- [ ] Index Maintenance: Rebuild indexes periodically to deal with bloat using `REINDEX`. | ||
- [ ] Indexing Join Columns: Index columns that are used in JOIN conditions to improve join performance. | ||
> `combineGroup` and `(r Role) Distinct()` | ||
|
||
### Future idea, _file archive content_ relationship table | ||
|
||
Create a relationship files table that contains the filename content within of every archive release. | ||
|
||
We could also include columns containing size in bytes, sha256 hash, text body for full text searching. | ||
|
||
This would replace the `file_zip_content` column and also, create a CLI tool to scan the archives to fill out this data. For saftey and code maintenance, the tool would need to be a separate program from the web server application. | ||
|
||
## Migration from MySQL to PostgreSQL | ||
|
||
This document describes how to migrate the Defacto2 MySQL database to PostgreSQL using [pgloader](https://pgloader.io/). Note, the migration is a one-time operation and should be run on a development or staging server before running on the production server. | ||
|
||
- `defacto2-inno` is the name of the MySQL database. | ||
- `defacto2_ps` is the name of the PostgreSQL database, note the `_` in the name as opposed to `-`. | ||
|
||
Create a migration loader file named `migrate.load` with the following content, replacing the connection strings with your own database credentials: | ||
|
||
```sql | ||
LOAD DATABASE | ||
FROM mysql://root:example@localhost:3306/defacto2-inno?useSSL=false | ||
INTO pgsql://root:example@localhost:5432/defacto2_ps | ||
|
||
WITH include drop, create tables | ||
|
||
ALTER SCHEMA 'source_db' RENAME TO 'public' | ||
; | ||
``` | ||
|
||
Run the migration using the following command: | ||
|
||
```sh | ||
# run the migration | ||
pgloader migrate.load | ||
|
||
# test the migration | ||
postgres psql | ||
``` | ||
|
||
```sql | ||
# SELECT * FROM files; | ||
``` | ||
|
||
A simple client application to interact with the migrated database is [Postbird](https://github.com/paxa/postbird). | ||
|
||
Some more resources: | ||
|
||
- [pgloader](https://pgloader.io/) | ||
- [documentation](https://pgloader.readthedocs.io/en/latest/) | ||
- [Docker hub](https://hub.docker.com/r/dimitri/pgloader/) | ||
- [DigitalOcean how-to migrate](https://www.digitalocean.com/community/tutorials/how-to-migrate-mysql-database-to-postgres-using-pgloader) | ||
- [Migrating from MySQL to PostgreSQL](https://pgloader.readthedocs.io/en/latest/tutorial/tutorial.html#migrating-from-mysql-to-postgresql) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
# Patterns | ||
|
||
## New | ||
|
||
This is a collection of patterns and idioms that could be useful with the new releases of Go. | ||
|
||
- https://go.dev/blog/go1.22 | ||
- https://tip.golang.org/doc/go1.22 | ||
- https://go.dev/blog/go1.21 | ||
- https://tip.golang.org/doc/go1.21 | ||
|
||
### Randomization in Go 1.22 | ||
|
||
New random package, [`math/rand/v2` package](https://pkg.go.dev/math/rand/v2). | ||
|
||
```go | ||
// print a random letter | ||
alpha := []string{ | ||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", | ||
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"} | ||
fmt.Println(alpha[rand.IntN(len(alpha))]) | ||
|
||
// print a random number between 0 and 100 | ||
fmt.Println(rand.IntN(100)) | ||
``` | ||
|
||
### New Null type in Go 1.22 | ||
|
||
New type `Null` for [database/sql package](https://pkg.go.dev/database/sql#Null). | ||
|
||
```go | ||
var s Null[string] | ||
err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&s) | ||
... | ||
if s.Valid { | ||
// use s.V | ||
} else { | ||
// NULL value | ||
} | ||
``` | ||
|
||
### New version comparison in Go 1.22 | ||
|
||
New [`go/version` package](https://pkg.go.dev/go/version) for version comparison. | ||
|
||
```go | ||
func Compare(x, y string) int | ||
func IsValid(x string) bool | ||
func Lang(x string) string | ||
``` | ||
|
||
### New built-in functions: min, max, clear in Go 1.21 | ||
|
||
```go | ||
x, y := 1, 100 | ||
m := min(x, y) | ||
M := max(x, y) | ||
c := max(5.0, y, 10) | ||
|
||
alpha := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"} | ||
clear(alpha) | ||
``` | ||
|
||
### New slices package in Go 1.21 | ||
|
||
New slices package for slice manipulation which are more efficient than `sort.`. | ||
https://pkg.go.dev/slices | ||
|
||
```go | ||
smallInts := []int8{0, 42, -10, 8} | ||
slices.Sort(smallInts) | ||
fmt.Println(smallInts) | ||
// Output: [-10 0 8 42] | ||
``` | ||
|
||
## Database queries | ||
|
||
The web application relies on an Object-relational mapping (ORM) implementation provided by [SQLBoiler](https://github.com/volatiletech/sqlboiler) to simplify development. | ||
|
||
Some tutorials for SQLBoiler include: | ||
|
||
- [SQL in Go with SQLBoiler](https://thedevelopercafe.com/articles/sql-in-go-with-sqlboiler-ac8efc4c5cb8) | ||
- [Introduction to SQLBoiler: Go framework for ORMs](https://blog.logrocket.com/introduction-sqlboiler-go-framework-orms/) | ||
|
||
--- | ||
|
||
Note, because I am lazy, the most of the descriptions below were generated using CoPilot AI. | ||
|
||
```go | ||
import( | ||
"github.com/Defacto2/server/internal/postgres/models" | ||
) | ||
|
||
got, err := models.Files().One(ctx, db) | ||
if err != nil { | ||
panic(err) | ||
} | ||
fmt.Println("got record:", got.ID) | ||
``` | ||
|
||
The above code snippet demonstrates how to query a single record from the `files` table. The `ctx` variable is a context.Context object and `db` is a *sql.DB object. | ||
|
||
#### Find a record by ID | ||
|
||
```go | ||
f := &models.File{ID: 1} | ||
found, err := models.FindFile(ctx, db, f.ID) | ||
if err != nil { | ||
log.DPanic(err) | ||
} | ||
fmt.Println("found record:", found.Filename) | ||
``` | ||
|
||
The above code snippet demonstrates how to query a single record from the `files` table using a specific ID. | ||
|
||
#### Find records by ID | ||
|
||
```go | ||
import( | ||
"github.com/Defacto2/server/internal/postgres/models" | ||
"github.com/volatiletech/sqlboiler/v4/queries/qm" | ||
) | ||
|
||
found, err := models.Files(qm.Where("id = ?", 1)).One(ctx, db) | ||
if err != nil { | ||
log.DPanic(err) | ||
} | ||
fmt.Println("found record:", found.Filename) | ||
``` | ||
|
||
The above code snippet also demonstrates how to query a single record from the `files` table using a specific ID. In this case, the query uses a query mod where clause. | ||
|
||
#### Check if a record exists | ||
|
||
```go | ||
exists, err := models.FileExists(ctx, db, 1) | ||
if err != nil { | ||
log.DPanic(err) | ||
} | ||
fmt.Println("found again exists?:", exists) | ||
``` | ||
|
||
The above code snippet demonstrates how to check if a record exists in the `files` table. | ||
|
||
#### Count records <small>with deleted</small> | ||
|
||
```go | ||
count, err := models.Files(qm.WithDeleted()).Count(ctx, db) | ||
if err != nil { | ||
log.DPanic(err) | ||
} | ||
countPub, err := models.Files().Count(ctx, db) | ||
if err != nil { | ||
log.DPanic(err) | ||
} | ||
fmt.Println("total files vs total files that are public:", count, "vs", countPub) | ||
``` | ||
|
||
The above code snippet demonstrates how to count the number of records in the `files` table. The first query counts all records, including those that have been soft-deleted. The second query counts only records that are not soft-deleted. | ||
|
||
#### Raw SQL queries | ||
|
||
```go | ||
var users []*models.File | ||
err := models.Files(qm.SQL(`select * from files;`)).Bind(ctx, db, &users) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
// range and print the results | ||
fmt.Print("raw files:") | ||
for i, user := range users { | ||
if i != 0 { | ||
fmt.Print(", ") | ||
} | ||
fmt.Print(user.Filename.String) | ||
} | ||
``` | ||
|
||
The above code snippet demonstrates how to execute a raw SQL query using SQLBoiler and bind the results to a slice of models. | ||
|
||
```go | ||
var users []*models.File | ||
rows, err := db.QueryContext(context.Background(), `select * from files;`) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
err = queries.Bind(rows, &users) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
rows.Close() | ||
``` | ||
|
||
The above code snippet demonstrates how to execute a raw SQL query and bind the results to a slice of models. The `queries.Bind` function is provided by SQLBoiler. |
Oops, something went wrong.