mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-08 02:39:05 +02:00
fix: export from SQLite fails to import into Postgres because of mismatched schemas
This commit is contained in:
@@ -160,38 +160,36 @@ func getScanValuesForTable(cols []string, types utils.DBSchemaTableTypes) []any
|
||||
// Store a pointer
|
||||
// Note: don't create a helper function for this switch, because it would return type "any" and mess everything up
|
||||
// If the column is nullable, we need a pointer to a pointer!
|
||||
switch types[col].Name {
|
||||
case "boolean", "bool":
|
||||
switch types[col].ExportKind() {
|
||||
case utils.DBExportKindBool:
|
||||
var x bool
|
||||
if types[col].Nullable {
|
||||
res[i] = new(new(x))
|
||||
} else {
|
||||
res[i] = new(x)
|
||||
}
|
||||
case "blob", "bytea", "jsonb":
|
||||
// Treat jsonb columns as binary too
|
||||
case utils.DBExportKindBytes:
|
||||
var x []byte
|
||||
if types[col].Nullable {
|
||||
res[i] = new(new(x))
|
||||
} else {
|
||||
res[i] = new(x)
|
||||
}
|
||||
case "timestamp", "timestamptz", "timestamp with time zone", "datetime":
|
||||
case utils.DBExportKindDateTime:
|
||||
var x datatype.DateTime
|
||||
if types[col].Nullable {
|
||||
res[i] = new(new(x))
|
||||
} else {
|
||||
res[i] = new(x)
|
||||
}
|
||||
case "integer", "int", "bigint":
|
||||
case utils.DBExportKindInteger:
|
||||
var x int64
|
||||
if types[col].Nullable {
|
||||
res[i] = new(new(x))
|
||||
} else {
|
||||
res[i] = new(x)
|
||||
}
|
||||
default:
|
||||
// Treat everything else as a string (including the "numeric" type)
|
||||
case utils.DBExportKindString:
|
||||
var x string
|
||||
if types[col].Nullable {
|
||||
res[i] = new(new(x))
|
||||
|
||||
@@ -367,8 +367,8 @@ func normalizeRowWithSchema(row map[string]any, table string, schema utils.DBSch
|
||||
|
||||
colType := schema[table][col]
|
||||
|
||||
switch colType.Name {
|
||||
case "timestamp", "timestamptz", "timestamp with time zone", "datetime":
|
||||
switch colType.ExportKind() {
|
||||
case utils.DBExportKindDateTime:
|
||||
// Dates are stored as strings
|
||||
str, ok := val.(string)
|
||||
if !ok {
|
||||
@@ -380,7 +380,7 @@ func normalizeRowWithSchema(row map[string]any, table string, schema utils.DBSch
|
||||
}
|
||||
row[col] = d
|
||||
|
||||
case "blob", "bytea", "jsonb":
|
||||
case utils.DBExportKindBytes:
|
||||
// Binary data and jsonb data is stored in the file as base64-encoded string
|
||||
str, ok := val.(string)
|
||||
if !ok {
|
||||
@@ -397,6 +397,9 @@ func normalizeRowWithSchema(row map[string]any, table string, schema utils.DBSch
|
||||
} else {
|
||||
row[col] = b
|
||||
}
|
||||
|
||||
case utils.DBExportKindString, utils.DBExportKindBool, utils.DBExportKindInteger:
|
||||
// JSON decodes these values into types the database accepts as they are
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
//go:build unit
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
|
||||
)
|
||||
|
||||
// TestDBSchemaParity ensures the SQLite and Postgres migrations produce schemas that encode every column the same way in a data export
|
||||
// Without this, an export taken from one database provider fails to import into the other (see https://github.com/pocket-id/pocket-id/issues/1603 and https://github.com/pocket-id/pocket-id/issues/1815)
|
||||
// If this test fails after adding a migration, pick column types for both databases that map to the same utils.DBExportKind
|
||||
func TestDBSchemaParity(t *testing.T) {
|
||||
sqliteSchema, err := utils.LoadDBSchemaTypes(testutils.NewDatabaseForTest(t))
|
||||
require.NoError(t, err)
|
||||
postgresSchema, err := utils.LoadDBSchemaTypes(testutils.NewPostgresDatabaseForTest(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
// The migration bookkeeping table is managed by golang-migrate and never exported
|
||||
delete(sqliteSchema, "schema_migrations")
|
||||
delete(postgresSchema, "schema_migrations")
|
||||
|
||||
// Both databases must have the same tables
|
||||
require.ElementsMatch(t, slices.Collect(maps.Keys(sqliteSchema)), slices.Collect(maps.Keys(postgresSchema)), "SQLite and Postgres must have the same tables")
|
||||
|
||||
for table, sqliteColumns := range sqliteSchema {
|
||||
postgresColumns := postgresSchema[table]
|
||||
|
||||
// Both databases must have the same columns in each table
|
||||
if !assert.ElementsMatchf(t, slices.Collect(maps.Keys(sqliteColumns)), slices.Collect(maps.Keys(postgresColumns)), "SQLite and Postgres must have the same columns in table %q", table) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Each column must be encoded the same way in an export and accept the same values
|
||||
for column, sqliteColumn := range sqliteColumns {
|
||||
postgresColumn := postgresColumns[column]
|
||||
assert.Equalf(t, sqliteColumn.ExportKind(), postgresColumn.ExportKind(),
|
||||
"column %s.%s is exported differently: SQLite type %q is %q, Postgres type %q is %q",
|
||||
table, column, sqliteColumn.Name, sqliteColumn.ExportKind(), postgresColumn.Name, postgresColumn.ExportKind())
|
||||
assert.Equalf(t, sqliteColumn.Nullable, postgresColumn.Nullable,
|
||||
"column %s.%s has a different nullability: SQLite nullable=%t, Postgres nullable=%t",
|
||||
table, column, sqliteColumn.Nullable, postgresColumn.Nullable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,38 @@ type DBSchemaColumn struct {
|
||||
Name string
|
||||
Nullable bool
|
||||
}
|
||||
|
||||
// DBExportKind is how the values of a column are encoded in a data export
|
||||
type DBExportKind string
|
||||
|
||||
const (
|
||||
DBExportKindString DBExportKind = "string"
|
||||
DBExportKindBool DBExportKind = "bool"
|
||||
DBExportKindInteger DBExportKind = "integer"
|
||||
DBExportKindDateTime DBExportKind = "datetime"
|
||||
// DBExportKindBytes values are stored as base64-encoded strings
|
||||
DBExportKindBytes DBExportKind = "bytes"
|
||||
)
|
||||
|
||||
// ExportKind returns how the values of the column are encoded in a data export
|
||||
// An export from one database provider can only be imported into another if every column has the same export kind on both
|
||||
func (c DBSchemaColumn) ExportKind() DBExportKind {
|
||||
switch c.Name {
|
||||
case "boolean", "bool":
|
||||
return DBExportKindBool
|
||||
case "integer", "int", "bigint":
|
||||
return DBExportKindInteger
|
||||
case "timestamp", "timestamptz", "timestamp with time zone", "datetime":
|
||||
return DBExportKindDateTime
|
||||
case "blob", "bytea", "jsonb":
|
||||
// jsonb is treated as binary too
|
||||
return DBExportKindBytes
|
||||
default:
|
||||
// Everything else is treated as a string, including the "numeric" type
|
||||
return DBExportKindString
|
||||
}
|
||||
}
|
||||
|
||||
type DBSchemaTableTypes = map[string]DBSchemaColumn
|
||||
type DBSchemaTypes = map[string]DBSchemaTableTypes
|
||||
|
||||
|
||||
@@ -5,20 +5,26 @@
|
||||
package testing
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
postgresMigrate "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
sqlitekit "github.com/italypaleale/go-sql-utils/sqlite"
|
||||
"github.com/libtnb/sqlite"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
@@ -62,6 +68,61 @@ func NewConcurrentDatabaseForTest(t *testing.T) *gorm.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
// NewPostgresDatabaseForTest returns a new instance of GORM connected to a fresh Postgres database with all Postgres migrations applied.
|
||||
// The server is taken from the POCKET_ID_TEST_POSTGRES_URL environment variable, and a new database is created on it for each test and dropped afterwards.
|
||||
// When the variable is not set the test is skipped, unless POCKET_ID_TEST_POSTGRES_REQUIRED is "true", which CI sets so these tests can never be skipped silently there.
|
||||
func NewPostgresDatabaseForTest(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
serverURL := os.Getenv("POCKET_ID_TEST_POSTGRES_URL")
|
||||
if serverURL == "" {
|
||||
if os.Getenv("POCKET_ID_TEST_POSTGRES_REQUIRED") == "true" {
|
||||
t.Fatal("POCKET_ID_TEST_POSTGRES_URL must be set when POCKET_ID_TEST_POSTGRES_REQUIRED is true")
|
||||
}
|
||||
t.Skip("POCKET_ID_TEST_POSTGRES_URL is not set")
|
||||
}
|
||||
|
||||
// Create a database that only this test uses, so tests can run in parallel against the same server
|
||||
admin, err := gorm.Open(postgres.Open(serverURL), newTestGormConfig(t))
|
||||
require.NoError(t, err, "Failed to connect to the Postgres test server")
|
||||
adminDB, err := admin.DB()
|
||||
require.NoError(t, err, "Failed to get sql.DB")
|
||||
dbName := "pocket_id_test_" + strings.ToLower(rand.Text())
|
||||
require.NoError(t, admin.Exec(`CREATE DATABASE "`+dbName+`"`).Error, "Failed to create the Postgres test database")
|
||||
|
||||
// Drop the database once the test is done, after its own connections have been closed by the cleanup registered below
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, admin.Exec(`DROP DATABASE IF EXISTS "`+dbName+`" WITH (FORCE)`).Error, "Failed to drop the Postgres test database")
|
||||
require.NoError(t, adminDB.Close(), "Failed to close the Postgres test server connection")
|
||||
})
|
||||
|
||||
// Connect to the new database by swapping the database name in the server URL
|
||||
u, err := url.Parse(serverURL)
|
||||
require.NoError(t, err, "Failed to parse POCKET_ID_TEST_POSTGRES_URL")
|
||||
u.Path = "/" + dbName
|
||||
db, err := gorm.Open(postgres.Open(u.String()), newTestGormConfig(t))
|
||||
require.NoError(t, err, "Failed to connect to the Postgres test database")
|
||||
sqlDB, err := db.DB()
|
||||
require.NoError(t, err, "Failed to get sql.DB")
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, sqlDB.Close(), "Failed to close the Postgres test database")
|
||||
})
|
||||
|
||||
// Apply the embedded Postgres migrations
|
||||
conn, err := sqlDB.Conn(t.Context())
|
||||
require.NoError(t, err, "Failed to acquire migration connection")
|
||||
defer conn.Close()
|
||||
driver, err := postgresMigrate.WithConnection(t.Context(), conn, &postgresMigrate.Config{})
|
||||
require.NoError(t, err, "Failed to create migration driver")
|
||||
source, err := iofs.New(resources.FS, "migrations/postgres")
|
||||
require.NoError(t, err, "Failed to create embedded migration source")
|
||||
m, err := migrate.NewWithInstance("iofs", source, "pocket-id", driver)
|
||||
require.NoError(t, err, "Failed to create migration instance")
|
||||
require.NoError(t, m.Up(), "Failed to perform migrations")
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// openInMemoryTestDB opens a GORM instance backed by an in-memory SQLite database, unique to the test.
|
||||
func openInMemoryTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE audit_logs ALTER COLUMN created_at DROP NOT NULL;
|
||||
ALTER TABLE audit_logs ALTER COLUMN user_agent DROP NOT NULL;
|
||||
ALTER TABLE kv ALTER COLUMN value DROP NOT NULL;
|
||||
ALTER TABLE oidc_clients ALTER COLUMN created_at DROP NOT NULL;
|
||||
ALTER TABLE users ALTER COLUMN first_name DROP NOT NULL;
|
||||
ALTER TABLE users ALTER COLUMN last_name DROP NOT NULL;
|
||||
ALTER TABLE webauthn_credentials ALTER COLUMN created_at DROP NOT NULL;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
-- Declare the columns that are NOT NULL on SQLite as NOT NULL too, so an export from either database can be imported into the other
|
||||
-- The application never writes NULL into these columns, but backfill any NULL left over from older versions first
|
||||
-- Empty strings are what the application writes for these values and what readers of kv treat as missing
|
||||
|
||||
UPDATE audit_logs SET created_at = now() WHERE created_at IS NULL;
|
||||
ALTER TABLE audit_logs ALTER COLUMN created_at SET NOT NULL;
|
||||
|
||||
UPDATE audit_logs SET user_agent = '' WHERE user_agent IS NULL;
|
||||
ALTER TABLE audit_logs ALTER COLUMN user_agent SET NOT NULL;
|
||||
|
||||
UPDATE kv SET value = '' WHERE value IS NULL;
|
||||
ALTER TABLE kv ALTER COLUMN value SET NOT NULL;
|
||||
|
||||
UPDATE oidc_clients SET created_at = now() WHERE created_at IS NULL;
|
||||
ALTER TABLE oidc_clients ALTER COLUMN created_at SET NOT NULL;
|
||||
|
||||
UPDATE users SET first_name = '' WHERE first_name IS NULL;
|
||||
ALTER TABLE users ALTER COLUMN first_name SET NOT NULL;
|
||||
|
||||
UPDATE users SET last_name = '' WHERE last_name IS NULL;
|
||||
ALTER TABLE users ALTER COLUMN last_name SET NOT NULL;
|
||||
|
||||
UPDATE webauthn_credentials SET created_at = now() WHERE created_at IS NULL;
|
||||
ALTER TABLE webauthn_credentials ALTER COLUMN created_at SET NOT NULL;
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
PRAGMA foreign_keys = OFF;
|
||||
BEGIN;
|
||||
|
||||
-- Restore the previous column types of webauthn_sessions.extensions and oauth2_sessions.rotated_at
|
||||
CREATE TABLE webauthn_sessions_new
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME,
|
||||
challenge TEXT NOT NULL UNIQUE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
user_verification TEXT NOT NULL,
|
||||
credential_params BLOB DEFAULT '[]' NOT NULL,
|
||||
extensions TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
INSERT INTO webauthn_sessions_new (
|
||||
id,
|
||||
created_at,
|
||||
challenge,
|
||||
expires_at,
|
||||
user_verification,
|
||||
credential_params,
|
||||
extensions
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
challenge,
|
||||
expires_at,
|
||||
user_verification,
|
||||
credential_params,
|
||||
extensions
|
||||
FROM webauthn_sessions;
|
||||
|
||||
DROP TABLE webauthn_sessions;
|
||||
ALTER TABLE webauthn_sessions_new RENAME TO webauthn_sessions;
|
||||
|
||||
CREATE INDEX idx_webauthn_sessions_expires_at ON webauthn_sessions (expires_at);
|
||||
|
||||
CREATE TABLE oauth2_sessions_new (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
request_id TEXT NOT NULL,
|
||||
client_id TEXT NOT NULL REFERENCES oidc_clients(id) ON DELETE CASCADE,
|
||||
access_token_signature TEXT NOT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
request_data BLOB NOT NULL,
|
||||
expires_at DATETIME,
|
||||
rotated_at INTEGER,
|
||||
CONSTRAINT chk_oauth2_sessions_client_id
|
||||
CHECK (client_id = json_extract(CAST(request_data AS TEXT), '$.client_id'))
|
||||
);
|
||||
|
||||
INSERT INTO oauth2_sessions_new (
|
||||
id,
|
||||
created_at,
|
||||
kind,
|
||||
key,
|
||||
request_id,
|
||||
client_id,
|
||||
access_token_signature,
|
||||
active,
|
||||
request_data,
|
||||
expires_at,
|
||||
rotated_at
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
kind,
|
||||
key,
|
||||
request_id,
|
||||
client_id,
|
||||
access_token_signature,
|
||||
active,
|
||||
request_data,
|
||||
expires_at,
|
||||
rotated_at
|
||||
FROM oauth2_sessions;
|
||||
|
||||
DROP TABLE oauth2_sessions;
|
||||
ALTER TABLE oauth2_sessions_new RENAME TO oauth2_sessions;
|
||||
|
||||
CREATE UNIQUE INDEX idx_oauth2_sessions_kind_key ON oauth2_sessions (kind, key);
|
||||
CREATE INDEX idx_oauth2_sessions_kind_request ON oauth2_sessions (kind, request_id);
|
||||
CREATE INDEX idx_oauth2_sessions_expires_at ON oauth2_sessions (expires_at);
|
||||
CREATE INDEX idx_oauth2_sessions_client_subject
|
||||
ON oauth2_sessions (client_id, json_extract(CAST(request_data AS TEXT), '$.session.subject'), kind, active);
|
||||
|
||||
-- Restore the TEXT primary keys without the NOT NULL constraint
|
||||
|
||||
CREATE TABLE api_keys_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
expires_at DATETIME NOT NULL,
|
||||
last_used_at DATETIME,
|
||||
created_at DATETIME,
|
||||
user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
|
||||
expiration_email_sent BOOLEAN NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
INSERT INTO api_keys_new (
|
||||
id,
|
||||
name,
|
||||
key,
|
||||
description,
|
||||
expires_at,
|
||||
last_used_at,
|
||||
created_at,
|
||||
user_id,
|
||||
expiration_email_sent
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
key,
|
||||
description,
|
||||
expires_at,
|
||||
last_used_at,
|
||||
created_at,
|
||||
user_id,
|
||||
expiration_email_sent
|
||||
FROM api_keys;
|
||||
|
||||
DROP TABLE api_keys;
|
||||
ALTER TABLE api_keys_new RENAME TO api_keys;
|
||||
|
||||
CREATE INDEX idx_api_keys_key ON api_keys(key);
|
||||
CREATE INDEX idx_api_keys_expires_at ON api_keys(expires_at);
|
||||
|
||||
CREATE TABLE audit_logs_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT NOT NULL,
|
||||
data BLOB NOT NULL,
|
||||
user_id TEXT REFERENCES users ON DELETE CASCADE,
|
||||
country TEXT,
|
||||
city TEXT
|
||||
);
|
||||
|
||||
INSERT INTO audit_logs_new (
|
||||
id,
|
||||
created_at,
|
||||
event,
|
||||
ip_address,
|
||||
user_agent,
|
||||
data,
|
||||
user_id,
|
||||
country,
|
||||
city
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
event,
|
||||
ip_address,
|
||||
user_agent,
|
||||
data,
|
||||
user_id,
|
||||
country,
|
||||
city
|
||||
FROM audit_logs;
|
||||
|
||||
DROP TABLE audit_logs;
|
||||
ALTER TABLE audit_logs_new RENAME TO audit_logs;
|
||||
|
||||
CREATE INDEX idx_audit_logs_client_name ON audit_logs((json_extract(data, '$.clientName')));
|
||||
CREATE INDEX idx_audit_logs_country ON audit_logs (country);
|
||||
CREATE INDEX idx_audit_logs_created_at ON audit_logs (created_at);
|
||||
CREATE INDEX idx_audit_logs_event ON audit_logs (event);
|
||||
CREATE INDEX idx_audit_logs_user_agent ON audit_logs (user_agent);
|
||||
CREATE INDEX idx_audit_logs_user_id ON audit_logs (user_id);
|
||||
|
||||
CREATE TABLE oidc_clients_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
name TEXT,
|
||||
callback_urls BLOB,
|
||||
image_type TEXT,
|
||||
created_by_id TEXT REFERENCES users ON DELETE SET NULL,
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
pkce_enabled BOOLEAN DEFAULT FALSE,
|
||||
logout_callback_urls BLOB,
|
||||
credentials BLOB,
|
||||
launch_url TEXT,
|
||||
requires_reauthentication BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
dark_image_type TEXT,
|
||||
is_group_restricted BOOLEAN NOT NULL DEFAULT 0,
|
||||
requires_pushed_authorization_requests BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
skip_consent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
pkce_supported BOOLEAN NOT NULL DEFAULT 0,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
client_type TEXT NOT NULL DEFAULT 'standard',
|
||||
metadata_expires_at DATETIME,
|
||||
metadata_grant_types BLOB,
|
||||
access_token_duration_minutes INTEGER NOT NULL DEFAULT 60,
|
||||
refresh_token_duration_minutes INTEGER NOT NULL DEFAULT 43200,
|
||||
backchannel_logout_url TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
INSERT INTO oidc_clients_new (
|
||||
id,
|
||||
created_at,
|
||||
name,
|
||||
callback_urls,
|
||||
image_type,
|
||||
created_by_id,
|
||||
is_public,
|
||||
pkce_enabled,
|
||||
logout_callback_urls,
|
||||
credentials,
|
||||
launch_url,
|
||||
requires_reauthentication,
|
||||
dark_image_type,
|
||||
is_group_restricted,
|
||||
requires_pushed_authorization_requests,
|
||||
skip_consent,
|
||||
pkce_supported,
|
||||
description,
|
||||
client_type,
|
||||
metadata_expires_at,
|
||||
metadata_grant_types,
|
||||
access_token_duration_minutes,
|
||||
refresh_token_duration_minutes,
|
||||
backchannel_logout_url
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
name,
|
||||
callback_urls,
|
||||
image_type,
|
||||
created_by_id,
|
||||
is_public,
|
||||
pkce_enabled,
|
||||
logout_callback_urls,
|
||||
credentials,
|
||||
launch_url,
|
||||
requires_reauthentication,
|
||||
dark_image_type,
|
||||
is_group_restricted,
|
||||
requires_pushed_authorization_requests,
|
||||
skip_consent,
|
||||
pkce_supported,
|
||||
description,
|
||||
client_type,
|
||||
metadata_expires_at,
|
||||
metadata_grant_types,
|
||||
access_token_duration_minutes,
|
||||
refresh_token_duration_minutes,
|
||||
backchannel_logout_url
|
||||
FROM oidc_clients;
|
||||
|
||||
DROP TABLE oidc_clients;
|
||||
ALTER TABLE oidc_clients_new RENAME TO oidc_clients;
|
||||
|
||||
CREATE TABLE reauthentication_tokens_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO reauthentication_tokens_new (
|
||||
id,
|
||||
created_at,
|
||||
token,
|
||||
expires_at,
|
||||
user_id
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
token,
|
||||
expires_at,
|
||||
user_id
|
||||
FROM reauthentication_tokens;
|
||||
|
||||
DROP TABLE reauthentication_tokens;
|
||||
ALTER TABLE reauthentication_tokens_new RENAME TO reauthentication_tokens;
|
||||
|
||||
CREATE INDEX idx_reauthentication_tokens_token ON reauthentication_tokens (token);
|
||||
CREATE INDEX idx_reauthentication_tokens_expires_at ON reauthentication_tokens (expires_at);
|
||||
|
||||
CREATE TABLE scim_service_providers_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
endpoint TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
last_synced_at DATETIME,
|
||||
oidc_client_id TEXT NOT NULL,
|
||||
FOREIGN KEY (oidc_client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO scim_service_providers_new (
|
||||
id,
|
||||
created_at,
|
||||
endpoint,
|
||||
token,
|
||||
last_synced_at,
|
||||
oidc_client_id
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
endpoint,
|
||||
token,
|
||||
last_synced_at,
|
||||
oidc_client_id
|
||||
FROM scim_service_providers;
|
||||
|
||||
DROP TABLE scim_service_providers;
|
||||
ALTER TABLE scim_service_providers_new RENAME TO scim_service_providers;
|
||||
|
||||
CREATE TABLE webauthn_credentials_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
credential_id BLOB NOT NULL UNIQUE,
|
||||
public_key BLOB NOT NULL,
|
||||
attestation_type TEXT NOT NULL,
|
||||
transport BLOB NOT NULL,
|
||||
user_id TEXT REFERENCES users ON DELETE CASCADE,
|
||||
backup_eligible BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
backup_state BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
aaguid TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'
|
||||
);
|
||||
|
||||
INSERT INTO webauthn_credentials_new (
|
||||
id,
|
||||
created_at,
|
||||
name,
|
||||
credential_id,
|
||||
public_key,
|
||||
attestation_type,
|
||||
transport,
|
||||
user_id,
|
||||
backup_eligible,
|
||||
backup_state,
|
||||
aaguid
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
name,
|
||||
credential_id,
|
||||
public_key,
|
||||
attestation_type,
|
||||
transport,
|
||||
user_id,
|
||||
backup_eligible,
|
||||
backup_state,
|
||||
aaguid
|
||||
FROM webauthn_credentials;
|
||||
|
||||
DROP TABLE webauthn_credentials;
|
||||
ALTER TABLE webauthn_credentials_new RENAME TO webauthn_credentials;
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys = ON;
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
PRAGMA foreign_keys = OFF;
|
||||
BEGIN;
|
||||
|
||||
-- Align the webauthn_sessions.extensions (JSONB) and oauth2_sessions.rotated_at (TIMESTAMPTZ) column types with the export format used for PostgreSQL
|
||||
CREATE TABLE webauthn_sessions_new
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME,
|
||||
challenge TEXT NOT NULL UNIQUE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
user_verification TEXT NOT NULL,
|
||||
credential_params BLOB DEFAULT '[]' NOT NULL,
|
||||
extensions BLOB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
INSERT INTO webauthn_sessions_new (
|
||||
id,
|
||||
created_at,
|
||||
challenge,
|
||||
expires_at,
|
||||
user_verification,
|
||||
credential_params,
|
||||
extensions
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
challenge,
|
||||
expires_at,
|
||||
user_verification,
|
||||
credential_params,
|
||||
extensions
|
||||
FROM webauthn_sessions;
|
||||
|
||||
DROP TABLE webauthn_sessions;
|
||||
ALTER TABLE webauthn_sessions_new RENAME TO webauthn_sessions;
|
||||
|
||||
CREATE INDEX idx_webauthn_sessions_expires_at ON webauthn_sessions (expires_at);
|
||||
|
||||
CREATE TABLE oauth2_sessions_new (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
request_id TEXT NOT NULL,
|
||||
client_id TEXT NOT NULL REFERENCES oidc_clients(id) ON DELETE CASCADE,
|
||||
access_token_signature TEXT NOT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
request_data BLOB NOT NULL,
|
||||
expires_at DATETIME,
|
||||
rotated_at DATETIME,
|
||||
CONSTRAINT chk_oauth2_sessions_client_id
|
||||
CHECK (client_id = json_extract(CAST(request_data AS TEXT), '$.client_id'))
|
||||
);
|
||||
|
||||
INSERT INTO oauth2_sessions_new (
|
||||
id,
|
||||
created_at,
|
||||
kind,
|
||||
key,
|
||||
request_id,
|
||||
client_id,
|
||||
access_token_signature,
|
||||
active,
|
||||
request_data,
|
||||
expires_at,
|
||||
rotated_at
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
kind,
|
||||
key,
|
||||
request_id,
|
||||
client_id,
|
||||
access_token_signature,
|
||||
active,
|
||||
request_data,
|
||||
expires_at,
|
||||
rotated_at
|
||||
FROM oauth2_sessions;
|
||||
|
||||
DROP TABLE oauth2_sessions;
|
||||
ALTER TABLE oauth2_sessions_new RENAME TO oauth2_sessions;
|
||||
|
||||
CREATE UNIQUE INDEX idx_oauth2_sessions_kind_key ON oauth2_sessions (kind, key);
|
||||
CREATE INDEX idx_oauth2_sessions_kind_request ON oauth2_sessions (kind, request_id);
|
||||
CREATE INDEX idx_oauth2_sessions_expires_at ON oauth2_sessions (expires_at);
|
||||
CREATE INDEX idx_oauth2_sessions_client_subject
|
||||
ON oauth2_sessions (client_id, json_extract(CAST(request_data AS TEXT), '$.session.subject'), kind, active);
|
||||
|
||||
-- Declare the TEXT primary keys as NOT NULL, because SQLite (unlike PostgreSQL) otherwise accepts NULL in them
|
||||
-- Rows with a NULL id cannot be referenced or addressed by the application, so they are dropped
|
||||
|
||||
CREATE TABLE api_keys_new
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
expires_at DATETIME NOT NULL,
|
||||
last_used_at DATETIME,
|
||||
created_at DATETIME,
|
||||
user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
|
||||
expiration_email_sent BOOLEAN NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
INSERT INTO api_keys_new (
|
||||
id,
|
||||
name,
|
||||
key,
|
||||
description,
|
||||
expires_at,
|
||||
last_used_at,
|
||||
created_at,
|
||||
user_id,
|
||||
expiration_email_sent
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
key,
|
||||
description,
|
||||
expires_at,
|
||||
last_used_at,
|
||||
created_at,
|
||||
user_id,
|
||||
expiration_email_sent
|
||||
FROM api_keys
|
||||
WHERE id IS NOT NULL;
|
||||
|
||||
DROP TABLE api_keys;
|
||||
ALTER TABLE api_keys_new RENAME TO api_keys;
|
||||
|
||||
CREATE INDEX idx_api_keys_key ON api_keys(key);
|
||||
CREATE INDEX idx_api_keys_expires_at ON api_keys(expires_at);
|
||||
|
||||
CREATE TABLE audit_logs_new
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT NOT NULL,
|
||||
data BLOB NOT NULL,
|
||||
user_id TEXT REFERENCES users ON DELETE CASCADE,
|
||||
country TEXT,
|
||||
city TEXT
|
||||
);
|
||||
|
||||
INSERT INTO audit_logs_new (
|
||||
id,
|
||||
created_at,
|
||||
event,
|
||||
ip_address,
|
||||
user_agent,
|
||||
data,
|
||||
user_id,
|
||||
country,
|
||||
city
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
event,
|
||||
ip_address,
|
||||
user_agent,
|
||||
data,
|
||||
user_id,
|
||||
country,
|
||||
city
|
||||
FROM audit_logs
|
||||
WHERE id IS NOT NULL;
|
||||
|
||||
DROP TABLE audit_logs;
|
||||
ALTER TABLE audit_logs_new RENAME TO audit_logs;
|
||||
|
||||
CREATE INDEX idx_audit_logs_client_name ON audit_logs((json_extract(data, '$.clientName')));
|
||||
CREATE INDEX idx_audit_logs_country ON audit_logs (country);
|
||||
CREATE INDEX idx_audit_logs_created_at ON audit_logs (created_at);
|
||||
CREATE INDEX idx_audit_logs_event ON audit_logs (event);
|
||||
CREATE INDEX idx_audit_logs_user_agent ON audit_logs (user_agent);
|
||||
CREATE INDEX idx_audit_logs_user_id ON audit_logs (user_id);
|
||||
|
||||
CREATE TABLE oidc_clients_new
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
name TEXT,
|
||||
callback_urls BLOB,
|
||||
image_type TEXT,
|
||||
created_by_id TEXT REFERENCES users ON DELETE SET NULL,
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
pkce_enabled BOOLEAN DEFAULT FALSE,
|
||||
logout_callback_urls BLOB,
|
||||
credentials BLOB,
|
||||
launch_url TEXT,
|
||||
requires_reauthentication BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
dark_image_type TEXT,
|
||||
is_group_restricted BOOLEAN NOT NULL DEFAULT 0,
|
||||
requires_pushed_authorization_requests BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
skip_consent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
pkce_supported BOOLEAN NOT NULL DEFAULT 0,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
client_type TEXT NOT NULL DEFAULT 'standard',
|
||||
metadata_expires_at DATETIME,
|
||||
metadata_grant_types BLOB,
|
||||
access_token_duration_minutes INTEGER NOT NULL DEFAULT 60,
|
||||
refresh_token_duration_minutes INTEGER NOT NULL DEFAULT 43200,
|
||||
backchannel_logout_url TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
INSERT INTO oidc_clients_new (
|
||||
id,
|
||||
created_at,
|
||||
name,
|
||||
callback_urls,
|
||||
image_type,
|
||||
created_by_id,
|
||||
is_public,
|
||||
pkce_enabled,
|
||||
logout_callback_urls,
|
||||
credentials,
|
||||
launch_url,
|
||||
requires_reauthentication,
|
||||
dark_image_type,
|
||||
is_group_restricted,
|
||||
requires_pushed_authorization_requests,
|
||||
skip_consent,
|
||||
pkce_supported,
|
||||
description,
|
||||
client_type,
|
||||
metadata_expires_at,
|
||||
metadata_grant_types,
|
||||
access_token_duration_minutes,
|
||||
refresh_token_duration_minutes,
|
||||
backchannel_logout_url
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
name,
|
||||
callback_urls,
|
||||
image_type,
|
||||
created_by_id,
|
||||
is_public,
|
||||
pkce_enabled,
|
||||
logout_callback_urls,
|
||||
credentials,
|
||||
launch_url,
|
||||
requires_reauthentication,
|
||||
dark_image_type,
|
||||
is_group_restricted,
|
||||
requires_pushed_authorization_requests,
|
||||
skip_consent,
|
||||
pkce_supported,
|
||||
description,
|
||||
client_type,
|
||||
metadata_expires_at,
|
||||
metadata_grant_types,
|
||||
access_token_duration_minutes,
|
||||
refresh_token_duration_minutes,
|
||||
backchannel_logout_url
|
||||
FROM oidc_clients
|
||||
WHERE id IS NOT NULL;
|
||||
|
||||
DROP TABLE oidc_clients;
|
||||
ALTER TABLE oidc_clients_new RENAME TO oidc_clients;
|
||||
|
||||
CREATE TABLE reauthentication_tokens_new
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO reauthentication_tokens_new (
|
||||
id,
|
||||
created_at,
|
||||
token,
|
||||
expires_at,
|
||||
user_id
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
token,
|
||||
expires_at,
|
||||
user_id
|
||||
FROM reauthentication_tokens
|
||||
WHERE id IS NOT NULL;
|
||||
|
||||
DROP TABLE reauthentication_tokens;
|
||||
ALTER TABLE reauthentication_tokens_new RENAME TO reauthentication_tokens;
|
||||
|
||||
CREATE INDEX idx_reauthentication_tokens_token ON reauthentication_tokens (token);
|
||||
CREATE INDEX idx_reauthentication_tokens_expires_at ON reauthentication_tokens (expires_at);
|
||||
|
||||
CREATE TABLE scim_service_providers_new
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
endpoint TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
last_synced_at DATETIME,
|
||||
oidc_client_id TEXT NOT NULL,
|
||||
FOREIGN KEY (oidc_client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO scim_service_providers_new (
|
||||
id,
|
||||
created_at,
|
||||
endpoint,
|
||||
token,
|
||||
last_synced_at,
|
||||
oidc_client_id
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
endpoint,
|
||||
token,
|
||||
last_synced_at,
|
||||
oidc_client_id
|
||||
FROM scim_service_providers
|
||||
WHERE id IS NOT NULL;
|
||||
|
||||
DROP TABLE scim_service_providers;
|
||||
ALTER TABLE scim_service_providers_new RENAME TO scim_service_providers;
|
||||
|
||||
CREATE TABLE webauthn_credentials_new
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
credential_id BLOB NOT NULL UNIQUE,
|
||||
public_key BLOB NOT NULL,
|
||||
attestation_type TEXT NOT NULL,
|
||||
transport BLOB NOT NULL,
|
||||
user_id TEXT REFERENCES users ON DELETE CASCADE,
|
||||
backup_eligible BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
backup_state BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
aaguid TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'
|
||||
);
|
||||
|
||||
INSERT INTO webauthn_credentials_new (
|
||||
id,
|
||||
created_at,
|
||||
name,
|
||||
credential_id,
|
||||
public_key,
|
||||
attestation_type,
|
||||
transport,
|
||||
user_id,
|
||||
backup_eligible,
|
||||
backup_state,
|
||||
aaguid
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
name,
|
||||
credential_id,
|
||||
public_key,
|
||||
attestation_type,
|
||||
transport,
|
||||
user_id,
|
||||
backup_eligible,
|
||||
backup_state,
|
||||
aaguid
|
||||
FROM webauthn_credentials
|
||||
WHERE id IS NOT NULL;
|
||||
|
||||
DROP TABLE webauthn_credentials;
|
||||
ALTER TABLE webauthn_credentials_new RENAME TO webauthn_credentials;
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys = ON;
|
||||
Reference in New Issue
Block a user