diff --git a/db/create.go b/db/create.go index c98370f..b272ecf 100644 --- a/db/create.go +++ b/db/create.go @@ -1,276 +1,272 @@ /* * Copyright © 2019-2020 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package db import ( "fmt" "strings" ) type ColumnType int type OptionalInt struct { Set bool Value int } type OptionalString struct { Set bool Value string } type SQLBuilder interface { ToSQL() (string, error) } type Column struct { Dialect DialectType Name string Nullable bool Default OptionalString Type ColumnType Size OptionalInt PrimaryKey bool } type CreateTableSqlBuilder struct { Dialect DialectType Name string IfNotExists bool ColumnOrder []string Columns map[string]*Column Constraints []string } const ( ColumnTypeBool ColumnType = iota ColumnTypeSmallInt ColumnType = iota ColumnTypeInteger ColumnType = iota ColumnTypeChar ColumnType = iota ColumnTypeVarChar ColumnType = iota ColumnTypeText ColumnType = iota ColumnTypeDateTime ColumnType = iota ) var _ SQLBuilder = &CreateTableSqlBuilder{} var UnsetSize OptionalInt = OptionalInt{Set: false, Value: 0} var UnsetDefault OptionalString = OptionalString{Set: false, Value: ""} func (d ColumnType) Format(dialect DialectType, size OptionalInt) (string, error) { if dialect != DialectMySQL && dialect != DialectPostgres && dialect != DialectSQLite { return "", fmt.Errorf("unsupported column type %d for dialect %d and size %v", d, dialect, size) } mod := "" if size.Set { mod = fmt.Sprintf("(%d)", size.Value) } switch d { case ColumnTypeSmallInt: switch dialect { case DialectSQLite: return "INTEGER", nil case DialectMySQL: return "SMALLINT" + mod, nil case DialectPostgres: return "SMALLINT", nil } case ColumnTypeInteger: switch dialect { case DialectSQLite, DialectPostgres: return "INTEGER", nil case DialectMySQL: return "INT" + mod, nil } case ColumnTypeChar: switch dialect { - case DialectSQLite: + case DialectSQLite, DialectPostgres: return "TEXT", nil case DialectMySQL: return "CHAR" + mod, nil - case DialectPostgres: - return "CHAR" + mod, nil } case ColumnTypeVarChar: switch dialect { - case DialectSQLite: + case DialectSQLite, DialectPostgres: return "TEXT", nil case DialectMySQL: return "VARCHAR" + mod, nil - case DialectPostgres: - return "VARCHAR" + mod, nil } case ColumnTypeBool: switch dialect { case DialectSQLite: return "INTEGER", nil case DialectMySQL: return "TINYINT(1)", nil case DialectPostgres: return "BOOLEAN", nil } case ColumnTypeDateTime: switch dialect { case DialectSQLite: return "DATETIME", nil case DialectMySQL: return "DATETIME", nil case DialectPostgres: return "TIMESTAMP", nil } case ColumnTypeText: switch dialect { case DialectSQLite, DialectMySQL, DialectPostgres: return "TEXT", nil } } return "", fmt.Errorf("unsupported column type %d for dialect %d and size %v", d, dialect, size) } func (c *Column) SetName(name string) *Column { c.Name = name return c } func (c *Column) SetNullable(nullable bool) *Column { c.Nullable = nullable return c } func (c *Column) SetPrimaryKey(pk bool) *Column { c.PrimaryKey = pk return c } func (c *Column) SetDefault(value string) *Column { c.Default = OptionalString{Set: true, Value: value} return c } func (c *Column) SetDefaultCurrentTimestamp() *Column { def := "" switch c.Dialect { case DialectSQLite, DialectPostgres: def = "CURRENT_TIMESTAMP" case DialectMySQL: def = "NOW()" } c.Default = OptionalString{Set: true, Value: def} return c } func (c *Column) SetType(t ColumnType) *Column { c.Type = t return c } func (c *Column) SetSize(size int) *Column { c.Size = OptionalInt{Set: true, Value: size} return c } func (c *Column) String() (string, error) { var str strings.Builder str.WriteString(c.Name) str.WriteString(" ") typeStr, err := c.Type.Format(c.Dialect, c.Size) if err != nil { return "", err } str.WriteString(typeStr) if !c.Nullable { str.WriteString(" NOT NULL") } if c.Default.Set { str.WriteString(" DEFAULT ") val := c.Default.Value if val == "" { val = "''" } str.WriteString(val) } if c.PrimaryKey { str.WriteString(" PRIMARY KEY") } return str.String(), nil } func (b *CreateTableSqlBuilder) Column(column *Column) *CreateTableSqlBuilder { if b.Columns == nil { b.Columns = make(map[string]*Column) } b.Columns[column.Name] = column b.ColumnOrder = append(b.ColumnOrder, column.Name) return b } func (b *CreateTableSqlBuilder) UniqueConstraint(columns ...string) *CreateTableSqlBuilder { for _, column := range columns { if _, ok := b.Columns[column]; !ok { // This fails silently. return b } } b.Constraints = append(b.Constraints, fmt.Sprintf("UNIQUE(%s)", strings.Join(columns, ","))) return b } func (b *CreateTableSqlBuilder) SetIfNotExists(ine bool) *CreateTableSqlBuilder { b.IfNotExists = ine return b } func (b *CreateTableSqlBuilder) ToSQL() (string, error) { var str strings.Builder str.WriteString("CREATE TABLE ") if b.IfNotExists { str.WriteString("IF NOT EXISTS ") } str.WriteString(b.Name) var things []string for _, columnName := range b.ColumnOrder { column, ok := b.Columns[columnName] if !ok { return "", fmt.Errorf("column not found: %s", columnName) } columnStr, err := column.String() if err != nil { return "", err } things = append(things, columnStr) } things = append(things, b.Constraints...) if thingLen := len(things); thingLen > 0 { str.WriteString(" ( ") for i, thing := range things { str.WriteString(thing) if i < thingLen-1 { str.WriteString(", ") } } str.WriteString(" )") } return str.String(), nil } diff --git a/postgres.sql b/postgres.sql index 1b9b14d..ebaee24 100644 --- a/postgres.sql +++ b/postgres.sql @@ -1,148 +1,148 @@ CREATE TABLE IF NOT EXISTS "accesstokens" ( "token" BYTEA PRIMARY KEY, "user_id" INT NOT NULL, "sudo" BOOLEAN NOT NULL DEFAULT FALSE, "one_time" BOOLEAN NOT NULL DEFAULT FALSE, "created" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "expires" TIMESTAMP DEFAULT NULL, "user_agent" TEXT DEFAULT NULL ); CREATE TABLE IF NOT EXISTS "appcontent" ( "id" TEXT PRIMARY KEY, "content" TEXT NOT NULL, "updated" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE "appmigrations" ( "version" INT NOT NULL, "migrated" TIMESTAMP NOT NULL, "result" TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS "collectionattributes" ( "collection_id" INT NOT NULL, "attribute" TEXT NOT NULL, "value" TEXT NOT NULL, PRIMARY KEY ("collection_id", "attribute") ); CREATE TABLE IF NOT EXISTS "collectionkeys" ( "collection_id" INT PRIMARY KEY, "public_key" BYTEA NOT NULL, "private_key" BYTEA NOT NULL ); CREATE TABLE IF NOT EXISTS "collectionpasswords" ( "collection_id" INT PRIMARY KEY, "password" TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS "collectionredirects" ( "prev_alias" TEXT NOT NULL PRIMARY KEY, "new_alias" TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS "collections" ( "id" SERIAL PRIMARY KEY, "alias" TEXT DEFAULT NULL, "title" TEXT NOT NULL, "description" TEXT NOT NULL, "style_sheet" TEXT, "script" TEXT DEFAULT NULL, "format" TEXT DEFAULT NULL, "privacy" INT NOT NULL, "owner_id" INT NOT NULL, "view_count" INT NOT NULL ); CREATE TABLE IF NOT EXISTS "posts" ( "id" TEXT PRIMARY KEY, "slug" TEXT DEFAULT NULL, "modify_token" TEXT DEFAULT NULL, "text_appearance" TEXT NOT NULL DEFAULT 'norm', "language" TEXT DEFAULT NULL, "rtl" BOOLEAN DEFAULT NULL, "privacy" INT NOT NULL, "owner_id" INT DEFAULT NULL, "collection_id" INT DEFAULT NULL, "pinned_position" SMALLINT NULL, "created" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "view_count" INT NOT NULL, "title" TEXT NOT NULL, "content" TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS "remotefollows" ( "collection_id" INT NOT NULL, "remote_user_id" INT NOT NULL, "created" TIMESTAMP NOT NULL, PRIMARY KEY ("collection_id", "remote_user_id") ); CREATE TABLE IF NOT EXISTS "remoteuserkeys" ( "id" TEXT PRIMARY KEY, "remote_user_id" INT NOT NULL, "public_key" BYTEA NOT NULL ); CREATE TABLE IF NOT EXISTS "remoteusers" ( "id" SERIAL PRIMARY KEY, "actor_id" TEXT NOT NULL, "inbox" TEXT NOT NULL, "shared_inbox" TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS "userattributes" ( "user_id" INT NOT NULL, "attribute" TEXT NOT NULL, "value" TEXT NOT NULL, PRIMARY KEY ("user_id", "attribute") ); CREATE TABLE "userinvites" ( "id" TEXT NOT NULL, "owner_id" INT NOT NULL, "max_uses" SMALLINT DEFAULT NULL, "created" TIMESTAMP NOT NULL, "expires" TIMESTAMP DEFAULT NULL, "inactive" BOOLEAN NOT NULL ); CREATE TABLE "users" ( "id" SERIAL PRIMARY KEY, "username" TEXT NOT NULL, "password" TEXT NOT NULL, - "email" BYTEA DEFAULT NULL, + "email" TEXT DEFAULT NULL, "created" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE "usersinvited" ( "invite_id" TEXT NOT NULL, "user_id" INT NOT NULL ); CREATE INDEX IF NOT EXISTS "alias" ON "collections" ("alias"); CREATE UNIQUE INDEX IF NOT EXISTS "id_slug" ON "posts" ("collection_id", "slug"); CREATE UNIQUE INDEX IF NOT EXISTS "owner_id" ON "posts" ("owner_id", "id"); CREATE INDEX IF NOT EXISTS "privacy_id" ON "posts" ("privacy", "id"); CREATE UNIQUE INDEX IF NOT EXISTS "remote_user_id" ON "remoteuserkeys" ("remote_user_id"); CREATE UNIQUE INDEX IF NOT EXISTS "actor_id" ON "remoteusers" ("actor_id"); CREATE UNIQUE INDEX IF NOT EXISTS "username" ON "users" ("username");