diff --git a/Makefile b/Makefile index e248ac5d..648adf0b 100644 --- a/Makefile +++ b/Makefile @@ -300,6 +300,14 @@ clean: clean-itest $(RM) ./litd-debug $(RM) coverage.txt +sqlc: + @$(call print, "Generating sql models and queries in Go") + ./scripts/gen_sqlc_docker.sh + +sqlc-check: sqlc + @$(call print, "Verifying sql code generation.") + if test -n "$$(git status --porcelain '*.go')"; then echo "SQL models not properly generated!"; git status --porcelain '*.go'; exit 1; fi + # Prevent make from interpreting any of the defined goals as folders or files to # include in the build process. .PHONY: default all yarn-install build install go-build go-build-noui \ diff --git a/db/sqlc/accounts.sql.go b/db/sqlc/accounts.sql.go new file mode 100644 index 00000000..4deefdb8 --- /dev/null +++ b/db/sqlc/accounts.sql.go @@ -0,0 +1,394 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: accounts.sql + +package sqlc + +import ( + "context" + "database/sql" + "time" +) + +const addAccountInvoice = `-- name: AddAccountInvoice :exec +INSERT INTO account_invoices (account_id, hash) +VALUES ($1, $2) +` + +type AddAccountInvoiceParams struct { + AccountID int64 + Hash []byte +} + +func (q *Queries) AddAccountInvoice(ctx context.Context, arg AddAccountInvoiceParams) error { + _, err := q.db.ExecContext(ctx, addAccountInvoice, arg.AccountID, arg.Hash) + return err +} + +const deleteAccount = `-- name: DeleteAccount :exec +DELETE FROM accounts +WHERE id = $1 +` + +func (q *Queries) DeleteAccount(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteAccount, id) + return err +} + +const deleteAccountPayment = `-- name: DeleteAccountPayment :exec +DELETE FROM account_payments +WHERE hash = $1 +AND account_id = $2 +` + +type DeleteAccountPaymentParams struct { + Hash []byte + AccountID int64 +} + +func (q *Queries) DeleteAccountPayment(ctx context.Context, arg DeleteAccountPaymentParams) error { + _, err := q.db.ExecContext(ctx, deleteAccountPayment, arg.Hash, arg.AccountID) + return err +} + +const getAccount = `-- name: GetAccount :one +SELECT id, alias, label, type, initial_balance_msat, current_balance_msat, last_updated, expiration +FROM accounts +WHERE id = $1 +` + +func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) { + row := q.db.QueryRowContext(ctx, getAccount, id) + var i Account + err := row.Scan( + &i.ID, + &i.Alias, + &i.Label, + &i.Type, + &i.InitialBalanceMsat, + &i.CurrentBalanceMsat, + &i.LastUpdated, + &i.Expiration, + ) + return i, err +} + +const getAccountByLabel = `-- name: GetAccountByLabel :one +SELECT id, alias, label, type, initial_balance_msat, current_balance_msat, last_updated, expiration +FROM accounts +WHERE label = $1 +` + +func (q *Queries) GetAccountByLabel(ctx context.Context, label sql.NullString) (Account, error) { + row := q.db.QueryRowContext(ctx, getAccountByLabel, label) + var i Account + err := row.Scan( + &i.ID, + &i.Alias, + &i.Label, + &i.Type, + &i.InitialBalanceMsat, + &i.CurrentBalanceMsat, + &i.LastUpdated, + &i.Expiration, + ) + return i, err +} + +const getAccountIDByAlias = `-- name: GetAccountIDByAlias :one +SELECT id +FROM accounts +WHERE alias = $1 +` + +func (q *Queries) GetAccountIDByAlias(ctx context.Context, alias int64) (int64, error) { + row := q.db.QueryRowContext(ctx, getAccountIDByAlias, alias) + var id int64 + err := row.Scan(&id) + return id, err +} + +const getAccountIndex = `-- name: GetAccountIndex :one +SELECT value +FROM account_indices +WHERE name = $1 +` + +func (q *Queries) GetAccountIndex(ctx context.Context, name string) (int64, error) { + row := q.db.QueryRowContext(ctx, getAccountIndex, name) + var value int64 + err := row.Scan(&value) + return value, err +} + +const getAccountInvoice = `-- name: GetAccountInvoice :one +SELECT account_id, hash +FROM account_invoices +WHERE account_id = $1 + AND hash = $2 +` + +type GetAccountInvoiceParams struct { + AccountID int64 + Hash []byte +} + +func (q *Queries) GetAccountInvoice(ctx context.Context, arg GetAccountInvoiceParams) (AccountInvoice, error) { + row := q.db.QueryRowContext(ctx, getAccountInvoice, arg.AccountID, arg.Hash) + var i AccountInvoice + err := row.Scan(&i.AccountID, &i.Hash) + return i, err +} + +const getAccountPayment = `-- name: GetAccountPayment :one +SELECT account_id, hash, status, full_amount_msat FROM account_payments +WHERE hash = $1 +AND account_id = $2 +` + +type GetAccountPaymentParams struct { + Hash []byte + AccountID int64 +} + +func (q *Queries) GetAccountPayment(ctx context.Context, arg GetAccountPaymentParams) (AccountPayment, error) { + row := q.db.QueryRowContext(ctx, getAccountPayment, arg.Hash, arg.AccountID) + var i AccountPayment + err := row.Scan( + &i.AccountID, + &i.Hash, + &i.Status, + &i.FullAmountMsat, + ) + return i, err +} + +const insertAccount = `-- name: InsertAccount :one +INSERT INTO accounts (type, initial_balance_msat, current_balance_msat, last_updated, label, alias, expiration) +VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id +` + +type InsertAccountParams struct { + Type int16 + InitialBalanceMsat int64 + CurrentBalanceMsat int64 + LastUpdated time.Time + Label sql.NullString + Alias int64 + Expiration time.Time +} + +func (q *Queries) InsertAccount(ctx context.Context, arg InsertAccountParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertAccount, + arg.Type, + arg.InitialBalanceMsat, + arg.CurrentBalanceMsat, + arg.LastUpdated, + arg.Label, + arg.Alias, + arg.Expiration, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const listAccountInvoices = `-- name: ListAccountInvoices :many +SELECT account_id, hash +FROM account_invoices +WHERE account_id = $1 +` + +func (q *Queries) ListAccountInvoices(ctx context.Context, accountID int64) ([]AccountInvoice, error) { + rows, err := q.db.QueryContext(ctx, listAccountInvoices, accountID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AccountInvoice + for rows.Next() { + var i AccountInvoice + if err := rows.Scan(&i.AccountID, &i.Hash); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listAccountPayments = `-- name: ListAccountPayments :many +SELECT account_id, hash, status, full_amount_msat +FROM account_payments +WHERE account_id = $1 +` + +func (q *Queries) ListAccountPayments(ctx context.Context, accountID int64) ([]AccountPayment, error) { + rows, err := q.db.QueryContext(ctx, listAccountPayments, accountID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AccountPayment + for rows.Next() { + var i AccountPayment + if err := rows.Scan( + &i.AccountID, + &i.Hash, + &i.Status, + &i.FullAmountMsat, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listAllAccounts = `-- name: ListAllAccounts :many +SELECT id, alias, label, type, initial_balance_msat, current_balance_msat, last_updated, expiration +FROM accounts +` + +func (q *Queries) ListAllAccounts(ctx context.Context) ([]Account, error) { + rows, err := q.db.QueryContext(ctx, listAllAccounts) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Account + for rows.Next() { + var i Account + if err := rows.Scan( + &i.ID, + &i.Alias, + &i.Label, + &i.Type, + &i.InitialBalanceMsat, + &i.CurrentBalanceMsat, + &i.LastUpdated, + &i.Expiration, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const setAccountIndex = `-- name: SetAccountIndex :exec +INSERT INTO account_indices (name, value) +VALUES ($1, $2) + ON CONFLICT (name) +DO UPDATE SET value = $2 +` + +type SetAccountIndexParams struct { + Name string + Value int64 +} + +func (q *Queries) SetAccountIndex(ctx context.Context, arg SetAccountIndexParams) error { + _, err := q.db.ExecContext(ctx, setAccountIndex, arg.Name, arg.Value) + return err +} + +const updateAccountBalance = `-- name: UpdateAccountBalance :one +UPDATE accounts +SET current_balance_msat = $1 +WHERE id = $2 +RETURNING id +` + +type UpdateAccountBalanceParams struct { + CurrentBalanceMsat int64 + ID int64 +} + +func (q *Queries) UpdateAccountBalance(ctx context.Context, arg UpdateAccountBalanceParams) (int64, error) { + row := q.db.QueryRowContext(ctx, updateAccountBalance, arg.CurrentBalanceMsat, arg.ID) + var id int64 + err := row.Scan(&id) + return id, err +} + +const updateAccountExpiry = `-- name: UpdateAccountExpiry :one +UPDATE accounts +SET expiration = $1 +WHERE id = $2 +RETURNING id +` + +type UpdateAccountExpiryParams struct { + Expiration time.Time + ID int64 +} + +func (q *Queries) UpdateAccountExpiry(ctx context.Context, arg UpdateAccountExpiryParams) (int64, error) { + row := q.db.QueryRowContext(ctx, updateAccountExpiry, arg.Expiration, arg.ID) + var id int64 + err := row.Scan(&id) + return id, err +} + +const updateAccountLastUpdate = `-- name: UpdateAccountLastUpdate :one +UPDATE accounts +SET last_updated = $1 +WHERE id = $2 +RETURNING id +` + +type UpdateAccountLastUpdateParams struct { + LastUpdated time.Time + ID int64 +} + +func (q *Queries) UpdateAccountLastUpdate(ctx context.Context, arg UpdateAccountLastUpdateParams) (int64, error) { + row := q.db.QueryRowContext(ctx, updateAccountLastUpdate, arg.LastUpdated, arg.ID) + var id int64 + err := row.Scan(&id) + return id, err +} + +const upsertAccountPayment = `-- name: UpsertAccountPayment :exec +INSERT INTO account_payments (account_id, hash, status, full_amount_msat) +VALUES ($1, $2, $3, $4) +ON CONFLICT (account_id, hash) +DO UPDATE SET status = $3, full_amount_msat = $4 +` + +type UpsertAccountPaymentParams struct { + AccountID int64 + Hash []byte + Status int16 + FullAmountMsat int64 +} + +func (q *Queries) UpsertAccountPayment(ctx context.Context, arg UpsertAccountPaymentParams) error { + _, err := q.db.ExecContext(ctx, upsertAccountPayment, + arg.AccountID, + arg.Hash, + arg.Status, + arg.FullAmountMsat, + ) + return err +} diff --git a/db/sqlc/db.go b/db/sqlc/db.go new file mode 100644 index 00000000..8ed64d13 --- /dev/null +++ b/db/sqlc/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package sqlc + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/db/sqlc/migrations/000001_accounts.down.sql b/db/sqlc/migrations/000001_accounts.down.sql new file mode 100644 index 00000000..da81c5df --- /dev/null +++ b/db/sqlc/migrations/000001_accounts.down.sql @@ -0,0 +1,4 @@ +DROP TABLE IF EXISTS account_payments; +DROP TABLE IF EXISTS account_invoices; +DROP TABLE IF EXISTS account_indices; +DROP TABLE IF EXISTS accounts; diff --git a/db/sqlc/migrations/000001_accounts.up.sql b/db/sqlc/migrations/000001_accounts.up.sql new file mode 100644 index 00000000..f9e17e16 --- /dev/null +++ b/db/sqlc/migrations/000001_accounts.up.sql @@ -0,0 +1,71 @@ +CREATE TABLE IF NOT EXISTS accounts ( + -- The auto incrementing primary key. + id INTEGER PRIMARY KEY, + + -- The ID that was used to identify the account in the legacy KVDB store. + -- In order to avoid breaking the API, we keep this field here so that + -- we can still look up accounts by this ID for the time being. + alias BIGINT NOT NULL UNIQUE, + + -- An optional label to use for the account. If it is set, it must be + -- unique. + label TEXT UNIQUE, + + -- The account type. + type SMALLINT NOT NULL, + + -- The accounts initial balance. This is never updated. + initial_balance_msat BIGINT NOT NULL, + + -- The accounts current balance. This is updated as the account is used. + current_balance_msat BIGINT NOT NULL, + + -- The last time the account was updated. + last_updated TIMESTAMP NOT NULL, + + -- The time that the account will expire. + expiration TIMESTAMP NOT NULL +); + +-- The account_payments table stores all the payment hashes of outgoing +-- payments that are associated with a particular account. These are used to +-- when an account should be debited. +CREATE TABLE IF NOT EXISTS account_payments ( + -- The account that this payment is linked to. + account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + + -- The payment hash of the payment. + hash BLOB NOT NULL, + + -- The LND RPC status of the payment. + status SMALLINT NOT NULL, + + -- The total amount of the payment in millisatoshis. + -- This includes the payment amount and estimated routing fee. + full_amount_msat BIGINT NOT NULL, + + UNIQUE(account_id, hash) +); + +-- The account_invoices table stores all the invoice payment hashes that +-- are associated with a particular account. These are used to determine +-- when an account should be credited. +CREATE TABLE IF NOT EXISTS account_invoices ( + -- The account that this invoice is linked to. + account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + + -- The payment hash of the invoice. + hash BLOB NOT NULL, + + UNIQUE(account_id, hash) +); + +-- The account_indices table stores any string-to-integer mappings that are +-- used by the accounts system. +CREATE TABLE IF NOT EXISTS account_indices ( + -- The unique name of the index. + name TEXT NOT NULL UNIQUE, + + -- The current value of the index. + value BIGINT NOT NULL +); \ No newline at end of file diff --git a/db/sqlc/models.go b/db/sqlc/models.go new file mode 100644 index 00000000..f11698e8 --- /dev/null +++ b/db/sqlc/models.go @@ -0,0 +1,38 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package sqlc + +import ( + "database/sql" + "time" +) + +type Account struct { + ID int64 + Alias int64 + Label sql.NullString + Type int16 + InitialBalanceMsat int64 + CurrentBalanceMsat int64 + LastUpdated time.Time + Expiration time.Time +} + +type AccountIndex struct { + Name string + Value int64 +} + +type AccountInvoice struct { + AccountID int64 + Hash []byte +} + +type AccountPayment struct { + AccountID int64 + Hash []byte + Status int16 + FullAmountMsat int64 +} diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go new file mode 100644 index 00000000..b0265c59 --- /dev/null +++ b/db/sqlc/querier.go @@ -0,0 +1,33 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package sqlc + +import ( + "context" + "database/sql" +) + +type Querier interface { + AddAccountInvoice(ctx context.Context, arg AddAccountInvoiceParams) error + DeleteAccount(ctx context.Context, id int64) error + DeleteAccountPayment(ctx context.Context, arg DeleteAccountPaymentParams) error + GetAccount(ctx context.Context, id int64) (Account, error) + GetAccountByLabel(ctx context.Context, label sql.NullString) (Account, error) + GetAccountIDByAlias(ctx context.Context, alias int64) (int64, error) + GetAccountIndex(ctx context.Context, name string) (int64, error) + GetAccountInvoice(ctx context.Context, arg GetAccountInvoiceParams) (AccountInvoice, error) + GetAccountPayment(ctx context.Context, arg GetAccountPaymentParams) (AccountPayment, error) + InsertAccount(ctx context.Context, arg InsertAccountParams) (int64, error) + ListAccountInvoices(ctx context.Context, accountID int64) ([]AccountInvoice, error) + ListAccountPayments(ctx context.Context, accountID int64) ([]AccountPayment, error) + ListAllAccounts(ctx context.Context) ([]Account, error) + SetAccountIndex(ctx context.Context, arg SetAccountIndexParams) error + UpdateAccountBalance(ctx context.Context, arg UpdateAccountBalanceParams) (int64, error) + UpdateAccountExpiry(ctx context.Context, arg UpdateAccountExpiryParams) (int64, error) + UpdateAccountLastUpdate(ctx context.Context, arg UpdateAccountLastUpdateParams) (int64, error) + UpsertAccountPayment(ctx context.Context, arg UpsertAccountPaymentParams) error +} + +var _ Querier = (*Queries)(nil) diff --git a/db/sqlc/queries/accounts.sql b/db/sqlc/queries/accounts.sql new file mode 100644 index 00000000..637a4972 --- /dev/null +++ b/db/sqlc/queries/accounts.sql @@ -0,0 +1,92 @@ +-- name: InsertAccount :one +INSERT INTO accounts (type, initial_balance_msat, current_balance_msat, last_updated, label, alias, expiration) +VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id; + +-- name: UpdateAccountBalance :one +UPDATE accounts +SET current_balance_msat = $1 +WHERE id = $2 +RETURNING id; + +-- name: UpdateAccountExpiry :one +UPDATE accounts +SET expiration = $1 +WHERE id = $2 +RETURNING id; + +-- name: UpdateAccountLastUpdate :one +UPDATE accounts +SET last_updated = $1 +WHERE id = $2 +RETURNING id; + +-- name: AddAccountInvoice :exec +INSERT INTO account_invoices (account_id, hash) +VALUES ($1, $2); + +-- name: DeleteAccountPayment :exec +DELETE FROM account_payments +WHERE hash = $1 +AND account_id = $2; + +-- name: UpsertAccountPayment :exec +INSERT INTO account_payments (account_id, hash, status, full_amount_msat) +VALUES ($1, $2, $3, $4) +ON CONFLICT (account_id, hash) +DO UPDATE SET status = $3, full_amount_msat = $4; + +-- name: GetAccountPayment :one +SELECT * FROM account_payments +WHERE hash = $1 +AND account_id = $2; + +-- name: GetAccount :one +SELECT * +FROM accounts +WHERE id = $1; + +-- name: GetAccountIDByAlias :one +SELECT id +FROM accounts +WHERE alias = $1; + +-- name: GetAccountByLabel :one +SELECT * +FROM accounts +WHERE label = $1; + +-- name: DeleteAccount :exec +DELETE FROM accounts +WHERE id = $1; + +-- name: ListAllAccounts :many +SELECT * +FROM accounts; + +-- name: ListAccountPayments :many +SELECT * +FROM account_payments +WHERE account_id = $1; + +-- name: ListAccountInvoices :many +SELECT * +FROM account_invoices +WHERE account_id = $1; + +-- name: GetAccountInvoice :one +SELECT * +FROM account_invoices +WHERE account_id = $1 + AND hash = $2; + +-- name: SetAccountIndex :exec +INSERT INTO account_indices (name, value) +VALUES ($1, $2) + ON CONFLICT (name) +DO UPDATE SET value = $2; + +-- name: GetAccountIndex :one +SELECT value +FROM account_indices +WHERE name = $1; diff --git a/scripts/gen_sqlc_docker.sh b/scripts/gen_sqlc_docker.sh new file mode 100755 index 00000000..16db97f2 --- /dev/null +++ b/scripts/gen_sqlc_docker.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -e + +# restore_files is a function to restore original schema files. +restore_files() { + echo "Restoring SQLite bigint patch..." + for file in db/sqlc/migrations/*.up.sql.bak; do + mv "$file" "${file%.bak}" + done +} + +# Set trap to call restore_files on script exit. This makes sure the old files +# are always restored. +trap restore_files EXIT + +# Directory of the script file, independent of where it's called from. +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Use the user's cache directories +GOCACHE=$(go env GOCACHE) +GOMODCACHE=$(go env GOMODCACHE) + +# SQLite doesn't support "BIGINT PRIMARY KEY" for auto-incrementing primary +# keys, only "INTEGER PRIMARY KEY". Internally it uses 64-bit integers for +# numbers anyway, independent of the column type. So we can just use +# "INTEGER PRIMARY KEY" and it will work the same under the hood, giving us +# auto incrementing 64-bit integers. +# _BUT_, sqlc will generate Go code with int32 if we use "INTEGER PRIMARY KEY", +# even though we want int64. So before we run sqlc, we need to patch the +# source schema SQL files to use "BIGINT PRIMARY KEY" instead of "INTEGER +# PRIMARY KEY". +echo "Applying SQLite bigint patch..." +for file in db/sqlc/migrations/*.up.sql; do + echo "Patching $file" + sed -i.bak -E 's/INTEGER PRIMARY KEY/BIGINT PRIMARY KEY/g' "$file" +done + +echo "Generating sql models and queries in go..." + +# Run the script to generate the new generated code. Once the script exits, we +# use `trap` to make sure all files are restored. +docker run \ + --rm \ + --user "$UID:$(id -g)" \ + -e UID=$UID \ + -v "$DIR/../:/build" \ + -w /build \ + sqlc/sqlc:1.25.0 generate diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 00000000..a6d8f2c5 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "db/sqlc/migrations" + queries: "db/sqlc/queries" + gen: + go: + out: db/sqlc + package: sqlc + emit_interface: true