mirror of
https://github.com/Rohithgilla12/data-peek
synced 2026-05-24 01:48:29 +00:00
Merge pull request #3 from Rohithgilla12/claude/add-mysql-support-014ADuBH4s69Gn3nAx1qPwcf
feat: add MySQL database support
This commit is contained in:
commit
8cf74ae473
12 changed files with 2398 additions and 730 deletions
|
|
@ -49,6 +49,7 @@
|
|||
"electron-updater": "^6.3.9",
|
||||
"lucide-react": "^0.555.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"mysql2": "^3.15.3",
|
||||
"pg": "^8.16.3",
|
||||
"sql-formatter": "^15.6.10",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
|
|
|
|||
689
apps/desktop/src/main/adapters/mysql-adapter.ts
Normal file
689
apps/desktop/src/main/adapters/mysql-adapter.ts
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
import mysql from 'mysql2/promise'
|
||||
import type {
|
||||
ConnectionConfig,
|
||||
SchemaInfo,
|
||||
TableInfo,
|
||||
ColumnInfo,
|
||||
QueryField,
|
||||
ForeignKeyInfo,
|
||||
TableDefinition,
|
||||
ColumnDefinition,
|
||||
ConstraintDefinition,
|
||||
IndexDefinition,
|
||||
SequenceInfo,
|
||||
CustomTypeInfo
|
||||
} from '@shared/index'
|
||||
import type { DatabaseAdapter, AdapterQueryResult, ExplainResult } from '../db-adapter'
|
||||
|
||||
/**
|
||||
* MySQL type codes to type name mapping
|
||||
* Based on mysql2 field type constants
|
||||
*/
|
||||
const MYSQL_TYPE_MAP: Record<number, string> = {
|
||||
0: 'decimal',
|
||||
1: 'tinyint',
|
||||
2: 'smallint',
|
||||
3: 'int',
|
||||
4: 'float',
|
||||
5: 'double',
|
||||
6: 'null',
|
||||
7: 'timestamp',
|
||||
8: 'bigint',
|
||||
9: 'mediumint',
|
||||
10: 'date',
|
||||
11: 'time',
|
||||
12: 'datetime',
|
||||
13: 'year',
|
||||
14: 'newdate',
|
||||
15: 'varchar',
|
||||
16: 'bit',
|
||||
245: 'json',
|
||||
246: 'newdecimal',
|
||||
247: 'enum',
|
||||
248: 'set',
|
||||
249: 'tiny_blob',
|
||||
250: 'medium_blob',
|
||||
251: 'long_blob',
|
||||
252: 'blob',
|
||||
253: 'var_string',
|
||||
254: 'string',
|
||||
255: 'geometry'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve MySQL type code to human-readable type name
|
||||
*/
|
||||
function resolveMySQLType(typeCode: number): string {
|
||||
return MYSQL_TYPE_MAP[typeCode] ?? `unknown(${typeCode})`
|
||||
}
|
||||
|
||||
/**
|
||||
* Create MySQL connection config from our ConnectionConfig
|
||||
*/
|
||||
function toMySQLConfig(config: ConnectionConfig): mysql.ConnectionOptions {
|
||||
return {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
ssl: config.ssl ? {} : undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize row from MySQL query to lowercase keys
|
||||
* MySQL can return column names in different cases depending on configuration
|
||||
*/
|
||||
function normalizeRow<T extends Record<string, unknown>>(row: Record<string, unknown>): T {
|
||||
const normalized: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
normalized[key.toLowerCase()] = value
|
||||
}
|
||||
return normalized as T
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL database adapter
|
||||
*/
|
||||
export class MySQLAdapter implements DatabaseAdapter {
|
||||
readonly dbType = 'mysql' as const
|
||||
|
||||
async connect(config: ConnectionConfig): Promise<void> {
|
||||
const connection = await mysql.createConnection(toMySQLConfig(config))
|
||||
await connection.end()
|
||||
}
|
||||
|
||||
async query(config: ConnectionConfig, sql: string): Promise<AdapterQueryResult> {
|
||||
const connection = await mysql.createConnection(toMySQLConfig(config))
|
||||
|
||||
try {
|
||||
const [rows, fields] = await connection.query(sql)
|
||||
|
||||
const queryFields: QueryField[] = (fields as mysql.FieldPacket[]).map((f) => ({
|
||||
name: f.name,
|
||||
dataType: resolveMySQLType(f.type ?? 253), // 253 = var_string as fallback
|
||||
dataTypeID: f.type ?? 253
|
||||
}))
|
||||
|
||||
const resultRows = Array.isArray(rows) ? rows : [rows]
|
||||
|
||||
return {
|
||||
rows: resultRows as Record<string, unknown>[],
|
||||
fields: queryFields,
|
||||
rowCount: resultRows.length
|
||||
}
|
||||
} finally {
|
||||
await connection.end()
|
||||
}
|
||||
}
|
||||
|
||||
async execute(
|
||||
config: ConnectionConfig,
|
||||
sql: string,
|
||||
params: unknown[]
|
||||
): Promise<{ rowCount: number | null }> {
|
||||
const connection = await mysql.createConnection(toMySQLConfig(config))
|
||||
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params)
|
||||
const affectedRows = (result as mysql.ResultSetHeader).affectedRows ?? null
|
||||
return { rowCount: affectedRows }
|
||||
} finally {
|
||||
await connection.end()
|
||||
}
|
||||
}
|
||||
|
||||
async executeTransaction(
|
||||
config: ConnectionConfig,
|
||||
statements: Array<{ sql: string; params: unknown[] }>
|
||||
): Promise<{ rowsAffected: number; results: Array<{ rowCount: number | null }> }> {
|
||||
const connection = await mysql.createConnection(toMySQLConfig(config))
|
||||
|
||||
try {
|
||||
await connection.beginTransaction()
|
||||
|
||||
const results: Array<{ rowCount: number | null }> = []
|
||||
let rowsAffected = 0
|
||||
|
||||
for (const stmt of statements) {
|
||||
const [result] = await connection.execute(stmt.sql, stmt.params)
|
||||
const affectedRows = (result as mysql.ResultSetHeader).affectedRows ?? 0
|
||||
results.push({ rowCount: affectedRows })
|
||||
rowsAffected += affectedRows
|
||||
}
|
||||
|
||||
await connection.commit()
|
||||
return { rowsAffected, results }
|
||||
} catch (error) {
|
||||
await connection.rollback().catch(() => {})
|
||||
throw error
|
||||
} finally {
|
||||
await connection.end()
|
||||
}
|
||||
}
|
||||
|
||||
async getSchemas(config: ConnectionConfig): Promise<SchemaInfo[]> {
|
||||
const connection = await mysql.createConnection(toMySQLConfig(config))
|
||||
|
||||
try {
|
||||
// In MySQL, "schema" = "database"
|
||||
// We'll show all databases as schemas, excluding system databases
|
||||
const [schemasRows] = await connection.query(`
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
|
||||
ORDER BY schema_name
|
||||
`)
|
||||
|
||||
const schemasRaw = schemasRows as Array<Record<string, unknown>>
|
||||
const schemas = schemasRaw.map((row) => normalizeRow<{ schema_name: string }>(row))
|
||||
|
||||
// Get all tables and views
|
||||
const [tablesRows] = await connection.query(`
|
||||
SELECT
|
||||
table_schema,
|
||||
table_name,
|
||||
table_type
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
|
||||
ORDER BY table_schema, table_name
|
||||
`)
|
||||
|
||||
const tablesRaw = tablesRows as Array<Record<string, unknown>>
|
||||
const tables = tablesRaw.map((row) =>
|
||||
normalizeRow<{
|
||||
table_schema: string
|
||||
table_name: string
|
||||
table_type: string
|
||||
}>(row)
|
||||
)
|
||||
|
||||
// Get all columns with primary key info
|
||||
const [columnsRows] = await connection.query(`
|
||||
SELECT
|
||||
c.table_schema,
|
||||
c.table_name,
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.column_type,
|
||||
c.is_nullable,
|
||||
c.column_default,
|
||||
c.ordinal_position,
|
||||
c.character_maximum_length,
|
||||
c.numeric_precision,
|
||||
c.numeric_scale,
|
||||
c.extra,
|
||||
CASE WHEN kcu.column_name IS NOT NULL THEN true ELSE false END as is_primary_key
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN information_schema.key_column_usage kcu
|
||||
ON c.table_schema = kcu.table_schema
|
||||
AND c.table_name = kcu.table_name
|
||||
AND c.column_name = kcu.column_name
|
||||
AND kcu.constraint_name = 'PRIMARY'
|
||||
WHERE c.table_schema NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
|
||||
ORDER BY c.table_schema, c.table_name, c.ordinal_position
|
||||
`)
|
||||
|
||||
const columnsRaw = columnsRows as Array<Record<string, unknown>>
|
||||
const columns = columnsRaw.map((row) =>
|
||||
normalizeRow<{
|
||||
table_schema: string
|
||||
table_name: string
|
||||
column_name: string
|
||||
data_type: string
|
||||
column_type: string
|
||||
is_nullable: string
|
||||
column_default: string | null
|
||||
ordinal_position: number
|
||||
character_maximum_length: number | null
|
||||
numeric_precision: number | null
|
||||
numeric_scale: number | null
|
||||
extra: string
|
||||
is_primary_key: number
|
||||
}>(row)
|
||||
)
|
||||
|
||||
// Get all foreign key relationships
|
||||
const [fkRows] = await connection.query(`
|
||||
SELECT
|
||||
kcu.table_schema,
|
||||
kcu.table_name,
|
||||
kcu.column_name,
|
||||
kcu.constraint_name,
|
||||
kcu.referenced_table_schema AS referenced_schema,
|
||||
kcu.referenced_table_name AS referenced_table,
|
||||
kcu.referenced_column_name AS referenced_column
|
||||
FROM information_schema.key_column_usage kcu
|
||||
WHERE kcu.referenced_table_name IS NOT NULL
|
||||
AND kcu.table_schema NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
|
||||
ORDER BY kcu.table_schema, kcu.table_name, kcu.column_name
|
||||
`)
|
||||
|
||||
const fkRaw = fkRows as Array<Record<string, unknown>>
|
||||
const foreignKeys = fkRaw.map((row) =>
|
||||
normalizeRow<{
|
||||
table_schema: string
|
||||
table_name: string
|
||||
column_name: string
|
||||
constraint_name: string
|
||||
referenced_schema: string
|
||||
referenced_table: string
|
||||
referenced_column: string
|
||||
}>(row)
|
||||
)
|
||||
|
||||
// Build foreign key lookup map
|
||||
const fkMap = new Map<string, ForeignKeyInfo>()
|
||||
for (const row of foreignKeys) {
|
||||
const key = `${row.table_schema}.${row.table_name}.${row.column_name}`
|
||||
fkMap.set(key, {
|
||||
constraintName: row.constraint_name,
|
||||
referencedSchema: row.referenced_schema,
|
||||
referencedTable: row.referenced_table,
|
||||
referencedColumn: row.referenced_column
|
||||
})
|
||||
}
|
||||
|
||||
// Build schema structure
|
||||
const schemaMap = new Map<string, SchemaInfo>()
|
||||
|
||||
for (const row of schemas) {
|
||||
schemaMap.set(row.schema_name, {
|
||||
name: row.schema_name,
|
||||
tables: []
|
||||
})
|
||||
}
|
||||
|
||||
// Build tables map
|
||||
const tableMap = new Map<string, TableInfo>()
|
||||
for (const row of tables) {
|
||||
const tableKey = `${row.table_schema}.${row.table_name}`
|
||||
const table: TableInfo = {
|
||||
name: row.table_name,
|
||||
type: row.table_type === 'VIEW' ? 'view' : 'table',
|
||||
columns: []
|
||||
}
|
||||
tableMap.set(tableKey, table)
|
||||
|
||||
const schema = schemaMap.get(row.table_schema)
|
||||
if (schema) {
|
||||
schema.tables.push(table)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign columns to tables
|
||||
for (const row of columns) {
|
||||
const tableKey = `${row.table_schema}.${row.table_name}`
|
||||
const table = tableMap.get(tableKey)
|
||||
if (table) {
|
||||
// Format data type with length/precision
|
||||
const dataType = row.column_type || row.data_type
|
||||
// MySQL column_type already includes size info like varchar(255)
|
||||
|
||||
const fkKey = `${row.table_schema}.${row.table_name}.${row.column_name}`
|
||||
const foreignKey = fkMap.get(fkKey)
|
||||
|
||||
// Handle auto_increment
|
||||
let defaultValue = row.column_default || undefined
|
||||
if (row.extra?.includes('auto_increment')) {
|
||||
defaultValue = 'auto_increment'
|
||||
}
|
||||
|
||||
const column: ColumnInfo = {
|
||||
name: row.column_name,
|
||||
dataType,
|
||||
isNullable: row.is_nullable === 'YES',
|
||||
isPrimaryKey: Boolean(row.is_primary_key),
|
||||
defaultValue,
|
||||
ordinalPosition: row.ordinal_position,
|
||||
foreignKey
|
||||
}
|
||||
table.columns.push(column)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(schemaMap.values())
|
||||
} finally {
|
||||
await connection.end()
|
||||
}
|
||||
}
|
||||
|
||||
async explain(config: ConnectionConfig, sql: string, analyze: boolean): Promise<ExplainResult> {
|
||||
const connection = await mysql.createConnection(toMySQLConfig(config))
|
||||
|
||||
try {
|
||||
// MySQL uses EXPLAIN ANALYZE (8.0.18+) or just EXPLAIN
|
||||
const explainQuery = analyze ? `EXPLAIN ANALYZE ${sql}` : `EXPLAIN FORMAT=JSON ${sql}`
|
||||
|
||||
const start = Date.now()
|
||||
const [rows] = await connection.query(explainQuery)
|
||||
const duration = Date.now() - start
|
||||
|
||||
// For JSON format, the result is in the first row
|
||||
let plan: unknown
|
||||
if (analyze) {
|
||||
// EXPLAIN ANALYZE returns text output
|
||||
plan = rows
|
||||
} else {
|
||||
// EXPLAIN FORMAT=JSON returns JSON in EXPLAIN column
|
||||
const resultRows = rows as Array<{ EXPLAIN: string }>
|
||||
if (resultRows.length > 0 && resultRows[0].EXPLAIN) {
|
||||
plan = JSON.parse(resultRows[0].EXPLAIN)
|
||||
} else {
|
||||
plan = rows
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
plan,
|
||||
durationMs: duration
|
||||
}
|
||||
} finally {
|
||||
await connection.end()
|
||||
}
|
||||
}
|
||||
|
||||
async getTableDDL(
|
||||
config: ConnectionConfig,
|
||||
schema: string,
|
||||
table: string
|
||||
): Promise<TableDefinition> {
|
||||
const connection = await mysql.createConnection(toMySQLConfig(config))
|
||||
|
||||
try {
|
||||
// Get columns with full metadata
|
||||
const [columnsRows] = await connection.query(
|
||||
`
|
||||
SELECT
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.column_type,
|
||||
c.is_nullable,
|
||||
c.column_default,
|
||||
c.ordinal_position,
|
||||
c.character_maximum_length,
|
||||
c.numeric_precision,
|
||||
c.numeric_scale,
|
||||
c.collation_name,
|
||||
c.column_comment,
|
||||
c.extra,
|
||||
CASE WHEN kcu.column_name IS NOT NULL THEN true ELSE false END as is_primary_key
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN information_schema.key_column_usage kcu
|
||||
ON c.table_schema = kcu.table_schema
|
||||
AND c.table_name = kcu.table_name
|
||||
AND c.column_name = kcu.column_name
|
||||
AND kcu.constraint_name = 'PRIMARY'
|
||||
WHERE c.table_schema = ? AND c.table_name = ?
|
||||
ORDER BY c.ordinal_position
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
const columnResults = columnsRows as Array<{
|
||||
column_name: string
|
||||
data_type: string
|
||||
column_type: string
|
||||
is_nullable: string
|
||||
column_default: string | null
|
||||
ordinal_position: number
|
||||
character_maximum_length: number | null
|
||||
numeric_precision: number | null
|
||||
numeric_scale: number | null
|
||||
collation_name: string | null
|
||||
column_comment: string | null
|
||||
extra: string
|
||||
is_primary_key: number
|
||||
}>
|
||||
|
||||
// Get constraints
|
||||
const [constraintsRows] = await connection.query(
|
||||
`
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
tc.constraint_type,
|
||||
kcu.column_name,
|
||||
kcu.referenced_table_schema AS ref_schema,
|
||||
kcu.referenced_table_name AS ref_table,
|
||||
kcu.referenced_column_name AS ref_column,
|
||||
rc.update_rule,
|
||||
rc.delete_rule
|
||||
FROM information_schema.table_constraints tc
|
||||
LEFT JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
AND tc.table_name = kcu.table_name
|
||||
LEFT JOIN information_schema.referential_constraints rc
|
||||
ON tc.constraint_name = rc.constraint_name
|
||||
AND tc.table_schema = rc.constraint_schema
|
||||
WHERE tc.table_schema = ? AND tc.table_name = ?
|
||||
ORDER BY tc.constraint_name, kcu.ordinal_position
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
const constraintResults = constraintsRows as Array<{
|
||||
constraint_name: string
|
||||
constraint_type: string
|
||||
column_name: string | null
|
||||
ref_schema: string | null
|
||||
ref_table: string | null
|
||||
ref_column: string | null
|
||||
update_rule: string | null
|
||||
delete_rule: string | null
|
||||
}>
|
||||
|
||||
// Get indexes
|
||||
const [indexesRows] = await connection.query(
|
||||
`
|
||||
SELECT
|
||||
index_name,
|
||||
non_unique,
|
||||
column_name,
|
||||
seq_in_index,
|
||||
index_type
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = ? AND table_name = ?
|
||||
AND index_name != 'PRIMARY'
|
||||
ORDER BY index_name, seq_in_index
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
const indexResults = indexesRows as Array<{
|
||||
index_name: string
|
||||
non_unique: number
|
||||
column_name: string
|
||||
seq_in_index: number
|
||||
index_type: string
|
||||
}>
|
||||
|
||||
// Get table comment
|
||||
const [tableCommentRows] = await connection.query(
|
||||
`
|
||||
SELECT table_comment
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ? AND table_name = ?
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
const tableCommentResult = tableCommentRows as Array<{ table_comment: string | null }>
|
||||
|
||||
// Build columns
|
||||
const columns: ColumnDefinition[] = columnResults.map((row, idx) => {
|
||||
let defaultValue = row.column_default || undefined
|
||||
if (row.extra?.includes('auto_increment')) {
|
||||
defaultValue = undefined // Will be handled as auto_increment
|
||||
}
|
||||
|
||||
return {
|
||||
id: `col-${idx}`,
|
||||
name: row.column_name,
|
||||
dataType: row.data_type,
|
||||
length: row.character_maximum_length || undefined,
|
||||
precision: row.numeric_precision || undefined,
|
||||
scale: row.numeric_scale || undefined,
|
||||
isNullable: row.is_nullable === 'YES',
|
||||
isPrimaryKey: Boolean(row.is_primary_key),
|
||||
isUnique: false, // Will be set from constraints
|
||||
defaultValue,
|
||||
comment: row.column_comment || undefined,
|
||||
collation: row.collation_name || undefined
|
||||
}
|
||||
})
|
||||
|
||||
// Build constraints
|
||||
const constraintMap = new Map<
|
||||
string,
|
||||
{
|
||||
type: string
|
||||
columns: string[]
|
||||
refSchema?: string
|
||||
refTable?: string
|
||||
refColumns?: string[]
|
||||
onUpdate?: string
|
||||
onDelete?: string
|
||||
}
|
||||
>()
|
||||
|
||||
for (const row of constraintResults) {
|
||||
const key = row.constraint_name
|
||||
if (!constraintMap.has(key)) {
|
||||
constraintMap.set(key, {
|
||||
type: row.constraint_type,
|
||||
columns: [],
|
||||
refSchema: row.ref_schema || undefined,
|
||||
refTable: row.ref_table || undefined,
|
||||
refColumns: [],
|
||||
onUpdate: row.update_rule || undefined,
|
||||
onDelete: row.delete_rule || undefined
|
||||
})
|
||||
}
|
||||
const constraint = constraintMap.get(key)!
|
||||
if (row.column_name && !constraint.columns.includes(row.column_name)) {
|
||||
constraint.columns.push(row.column_name)
|
||||
}
|
||||
if (row.ref_column && !constraint.refColumns!.includes(row.ref_column)) {
|
||||
constraint.refColumns!.push(row.ref_column)
|
||||
}
|
||||
}
|
||||
|
||||
const constraints: ConstraintDefinition[] = []
|
||||
let constraintIdx = 0
|
||||
for (const [name, data] of constraintMap) {
|
||||
if (data.type === 'PRIMARY KEY') continue
|
||||
|
||||
const constraintDef: ConstraintDefinition = {
|
||||
id: `constraint-${constraintIdx++}`,
|
||||
name,
|
||||
type: data.type === 'FOREIGN KEY' ? 'foreign_key' : 'unique',
|
||||
columns: data.columns
|
||||
}
|
||||
|
||||
if (data.type === 'FOREIGN KEY') {
|
||||
constraintDef.referencedSchema = data.refSchema
|
||||
constraintDef.referencedTable = data.refTable
|
||||
constraintDef.referencedColumns = data.refColumns
|
||||
constraintDef.onUpdate = data.onUpdate as ConstraintDefinition['onUpdate']
|
||||
constraintDef.onDelete = data.onDelete as ConstraintDefinition['onDelete']
|
||||
}
|
||||
|
||||
if (data.type === 'UNIQUE' && data.columns.length === 1) {
|
||||
const col = columns.find((c) => c.name === data.columns[0])
|
||||
if (col) col.isUnique = true
|
||||
}
|
||||
|
||||
constraints.push(constraintDef)
|
||||
}
|
||||
|
||||
// Build indexes
|
||||
const indexMap = new Map<string, { isUnique: boolean; method: string; columns: string[] }>()
|
||||
for (const row of indexResults) {
|
||||
if (!indexMap.has(row.index_name)) {
|
||||
indexMap.set(row.index_name, {
|
||||
isUnique: !row.non_unique,
|
||||
method: row.index_type.toLowerCase(),
|
||||
columns: []
|
||||
})
|
||||
}
|
||||
indexMap.get(row.index_name)!.columns.push(row.column_name)
|
||||
}
|
||||
|
||||
const indexes: IndexDefinition[] = []
|
||||
let indexIdx = 0
|
||||
for (const [name, data] of indexMap) {
|
||||
indexes.push({
|
||||
id: `index-${indexIdx++}`,
|
||||
name,
|
||||
columns: data.columns.map((c) => ({ name: c })),
|
||||
isUnique: data.isUnique,
|
||||
method: data.method as IndexDefinition['method']
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
schema,
|
||||
name: table,
|
||||
columns,
|
||||
constraints,
|
||||
indexes,
|
||||
comment: tableCommentResult[0]?.table_comment || undefined
|
||||
}
|
||||
} finally {
|
||||
await connection.end()
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getSequences(_config: ConnectionConfig): Promise<SequenceInfo[]> {
|
||||
// MySQL doesn't have sequences - it uses AUTO_INCREMENT
|
||||
// Return empty array as sequences are a PostgreSQL concept
|
||||
return []
|
||||
}
|
||||
|
||||
async getTypes(config: ConnectionConfig): Promise<CustomTypeInfo[]> {
|
||||
// Get MySQL ENUM types from columns
|
||||
const connection = await mysql.createConnection(toMySQLConfig(config))
|
||||
|
||||
try {
|
||||
// MySQL doesn't have standalone enum types, they're defined per column
|
||||
// We'll extract unique enum definitions from columns
|
||||
const [enumRows] = await connection.query(`
|
||||
SELECT DISTINCT
|
||||
table_schema as schema_name,
|
||||
column_type
|
||||
FROM information_schema.columns
|
||||
WHERE data_type = 'enum'
|
||||
AND table_schema NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
|
||||
ORDER BY table_schema, column_type
|
||||
`)
|
||||
|
||||
const enums = enumRows as Array<{ schema_name: string; column_type: string }>
|
||||
|
||||
const types: CustomTypeInfo[] = []
|
||||
let idx = 0
|
||||
|
||||
for (const row of enums) {
|
||||
// Parse enum values from column_type like "enum('a','b','c')"
|
||||
const match = row.column_type.match(/^enum\((.*)\)$/i)
|
||||
if (match) {
|
||||
const valuesStr = match[1]
|
||||
const values = valuesStr.split(',').map((v) => v.replace(/^'|'$/g, ''))
|
||||
|
||||
types.push({
|
||||
schema: row.schema_name,
|
||||
name: `enum_${idx++}`,
|
||||
type: 'enum',
|
||||
values
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return types
|
||||
} finally {
|
||||
await connection.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
667
apps/desktop/src/main/adapters/postgres-adapter.ts
Normal file
667
apps/desktop/src/main/adapters/postgres-adapter.ts
Normal file
|
|
@ -0,0 +1,667 @@
|
|||
import { Client } from 'pg'
|
||||
import type {
|
||||
ConnectionConfig,
|
||||
SchemaInfo,
|
||||
TableInfo,
|
||||
ColumnInfo,
|
||||
QueryField,
|
||||
ForeignKeyInfo,
|
||||
TableDefinition,
|
||||
ColumnDefinition,
|
||||
ConstraintDefinition,
|
||||
IndexDefinition,
|
||||
SequenceInfo,
|
||||
CustomTypeInfo
|
||||
} from '@shared/index'
|
||||
import type { DatabaseAdapter, AdapterQueryResult, ExplainResult } from '../db-adapter'
|
||||
|
||||
/**
|
||||
* PostgreSQL OID to Type Name Mapping
|
||||
* Reference: https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.dat
|
||||
*/
|
||||
const PG_TYPE_MAP: Record<number, string> = {
|
||||
16: 'boolean',
|
||||
17: 'bytea',
|
||||
18: 'char',
|
||||
19: 'name',
|
||||
20: 'bigint',
|
||||
21: 'smallint',
|
||||
23: 'integer',
|
||||
24: 'regproc',
|
||||
25: 'text',
|
||||
26: 'oid',
|
||||
114: 'json',
|
||||
142: 'xml',
|
||||
600: 'point',
|
||||
601: 'lseg',
|
||||
602: 'path',
|
||||
603: 'box',
|
||||
604: 'polygon',
|
||||
628: 'line',
|
||||
700: 'real',
|
||||
701: 'double precision',
|
||||
718: 'circle',
|
||||
790: 'money',
|
||||
829: 'macaddr',
|
||||
869: 'inet',
|
||||
650: 'cidr',
|
||||
1042: 'char',
|
||||
1043: 'varchar',
|
||||
1082: 'date',
|
||||
1083: 'time',
|
||||
1114: 'timestamp',
|
||||
1184: 'timestamptz',
|
||||
1186: 'interval',
|
||||
1266: 'timetz',
|
||||
1560: 'bit',
|
||||
1562: 'varbit',
|
||||
1700: 'numeric',
|
||||
2950: 'uuid',
|
||||
3802: 'jsonb',
|
||||
3904: 'int4range',
|
||||
3906: 'numrange',
|
||||
3908: 'tsrange',
|
||||
3910: 'tstzrange',
|
||||
3912: 'daterange',
|
||||
3926: 'int8range',
|
||||
// Array types (common ones)
|
||||
1000: 'boolean[]',
|
||||
1001: 'bytea[]',
|
||||
1005: 'smallint[]',
|
||||
1007: 'integer[]',
|
||||
1009: 'text[]',
|
||||
1014: 'char[]',
|
||||
1015: 'varchar[]',
|
||||
1016: 'bigint[]',
|
||||
1021: 'real[]',
|
||||
1022: 'double precision[]',
|
||||
1028: 'oid[]',
|
||||
1115: 'timestamp[]',
|
||||
1182: 'date[]',
|
||||
1183: 'time[]',
|
||||
1231: 'numeric[]',
|
||||
2951: 'uuid[]',
|
||||
3807: 'jsonb[]',
|
||||
199: 'json[]'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve PostgreSQL OID to human-readable type name
|
||||
*/
|
||||
function resolvePostgresType(dataTypeID: number): string {
|
||||
return PG_TYPE_MAP[dataTypeID] ?? `unknown(${dataTypeID})`
|
||||
}
|
||||
|
||||
/**
|
||||
* PostgreSQL database adapter
|
||||
*/
|
||||
export class PostgresAdapter implements DatabaseAdapter {
|
||||
readonly dbType = 'postgresql' as const
|
||||
|
||||
async connect(config: ConnectionConfig): Promise<void> {
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
await client.end()
|
||||
}
|
||||
|
||||
async query(config: ConnectionConfig, sql: string): Promise<AdapterQueryResult> {
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
|
||||
try {
|
||||
const res = await client.query(sql)
|
||||
|
||||
const fields: QueryField[] = res.fields.map((f) => ({
|
||||
name: f.name,
|
||||
dataType: resolvePostgresType(f.dataTypeID),
|
||||
dataTypeID: f.dataTypeID
|
||||
}))
|
||||
|
||||
return {
|
||||
rows: res.rows,
|
||||
fields,
|
||||
rowCount: res.rowCount
|
||||
}
|
||||
} finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
async execute(
|
||||
config: ConnectionConfig,
|
||||
sql: string,
|
||||
params: unknown[]
|
||||
): Promise<{ rowCount: number | null }> {
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
|
||||
try {
|
||||
const res = await client.query(sql, params)
|
||||
return { rowCount: res.rowCount }
|
||||
} finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
async executeTransaction(
|
||||
config: ConnectionConfig,
|
||||
statements: Array<{ sql: string; params: unknown[] }>
|
||||
): Promise<{ rowsAffected: number; results: Array<{ rowCount: number | null }> }> {
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
|
||||
try {
|
||||
await client.query('BEGIN')
|
||||
|
||||
const results: Array<{ rowCount: number | null }> = []
|
||||
let rowsAffected = 0
|
||||
|
||||
for (const stmt of statements) {
|
||||
const res = await client.query(stmt.sql, stmt.params)
|
||||
results.push({ rowCount: res.rowCount })
|
||||
rowsAffected += res.rowCount ?? 0
|
||||
}
|
||||
|
||||
await client.query('COMMIT')
|
||||
return { rowsAffected, results }
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK').catch(() => {})
|
||||
throw error
|
||||
} finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
async getSchemas(config: ConnectionConfig): Promise<SchemaInfo[]> {
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
|
||||
try {
|
||||
// Query 1: Get all schemas (excluding system schemas)
|
||||
const schemasResult = await client.query(`
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
||||
ORDER BY schema_name
|
||||
`)
|
||||
|
||||
// Query 2: Get all tables and views
|
||||
const tablesResult = await client.query(`
|
||||
SELECT
|
||||
table_schema,
|
||||
table_name,
|
||||
table_type
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
||||
ORDER BY table_schema, table_name
|
||||
`)
|
||||
|
||||
// Query 3: Get all columns with primary key info
|
||||
const columnsResult = await client.query(`
|
||||
SELECT
|
||||
c.table_schema,
|
||||
c.table_name,
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.udt_name,
|
||||
c.is_nullable,
|
||||
c.column_default,
|
||||
c.ordinal_position,
|
||||
c.character_maximum_length,
|
||||
c.numeric_precision,
|
||||
c.numeric_scale,
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
kcu.table_schema,
|
||||
kcu.table_name,
|
||||
kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
WHERE tc.constraint_type = 'PRIMARY KEY'
|
||||
) pk ON c.table_schema = pk.table_schema
|
||||
AND c.table_name = pk.table_name
|
||||
AND c.column_name = pk.column_name
|
||||
WHERE c.table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
||||
ORDER BY c.table_schema, c.table_name, c.ordinal_position
|
||||
`)
|
||||
|
||||
// Query 4: Get all foreign key relationships
|
||||
const foreignKeysResult = await client.query(`
|
||||
SELECT
|
||||
tc.table_schema,
|
||||
tc.table_name,
|
||||
kcu.column_name,
|
||||
tc.constraint_name,
|
||||
ccu.table_schema AS referenced_schema,
|
||||
ccu.table_name AS referenced_table,
|
||||
ccu.column_name AS referenced_column
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.constraint_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
||||
ORDER BY tc.table_schema, tc.table_name, kcu.column_name
|
||||
`)
|
||||
|
||||
// Build foreign key lookup map: "schema.table.column" -> ForeignKeyInfo
|
||||
const fkMap = new Map<string, ForeignKeyInfo>()
|
||||
for (const row of foreignKeysResult.rows) {
|
||||
const key = `${row.table_schema}.${row.table_name}.${row.column_name}`
|
||||
fkMap.set(key, {
|
||||
constraintName: row.constraint_name,
|
||||
referencedSchema: row.referenced_schema,
|
||||
referencedTable: row.referenced_table,
|
||||
referencedColumn: row.referenced_column
|
||||
})
|
||||
}
|
||||
|
||||
// Build schema structure
|
||||
const schemaMap = new Map<string, SchemaInfo>()
|
||||
|
||||
// Initialize schemas
|
||||
for (const row of schemasResult.rows) {
|
||||
schemaMap.set(row.schema_name, {
|
||||
name: row.schema_name,
|
||||
tables: []
|
||||
})
|
||||
}
|
||||
|
||||
// Build tables map for easy column assignment
|
||||
const tableMap = new Map<string, TableInfo>()
|
||||
for (const row of tablesResult.rows) {
|
||||
const tableKey = `${row.table_schema}.${row.table_name}`
|
||||
const table: TableInfo = {
|
||||
name: row.table_name,
|
||||
type: row.table_type === 'VIEW' ? 'view' : 'table',
|
||||
columns: []
|
||||
}
|
||||
tableMap.set(tableKey, table)
|
||||
|
||||
// Add table to its schema
|
||||
const schema = schemaMap.get(row.table_schema)
|
||||
if (schema) {
|
||||
schema.tables.push(table)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign columns to tables
|
||||
for (const row of columnsResult.rows) {
|
||||
const tableKey = `${row.table_schema}.${row.table_name}`
|
||||
const table = tableMap.get(tableKey)
|
||||
if (table) {
|
||||
// Format data type nicely
|
||||
let dataType = row.udt_name
|
||||
if (row.character_maximum_length) {
|
||||
dataType = `${row.udt_name}(${row.character_maximum_length})`
|
||||
} else if (row.numeric_precision && row.numeric_scale) {
|
||||
dataType = `${row.udt_name}(${row.numeric_precision},${row.numeric_scale})`
|
||||
}
|
||||
|
||||
// Check for foreign key relationship
|
||||
const fkKey = `${row.table_schema}.${row.table_name}.${row.column_name}`
|
||||
const foreignKey = fkMap.get(fkKey)
|
||||
|
||||
const column: ColumnInfo = {
|
||||
name: row.column_name,
|
||||
dataType,
|
||||
isNullable: row.is_nullable === 'YES',
|
||||
isPrimaryKey: row.is_primary_key,
|
||||
defaultValue: row.column_default || undefined,
|
||||
ordinalPosition: row.ordinal_position,
|
||||
foreignKey
|
||||
}
|
||||
table.columns.push(column)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(schemaMap.values())
|
||||
} finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
async explain(config: ConnectionConfig, sql: string, analyze: boolean): Promise<ExplainResult> {
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
|
||||
try {
|
||||
const explainOptions = analyze
|
||||
? 'ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON'
|
||||
: 'COSTS, VERBOSE, FORMAT JSON'
|
||||
const explainQuery = `EXPLAIN (${explainOptions}) ${sql}`
|
||||
|
||||
const start = Date.now()
|
||||
const res = await client.query(explainQuery)
|
||||
const duration = Date.now() - start
|
||||
|
||||
const planJson = res.rows[0]?.['QUERY PLAN']
|
||||
|
||||
return {
|
||||
plan: planJson,
|
||||
durationMs: duration
|
||||
}
|
||||
} finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
async getTableDDL(
|
||||
config: ConnectionConfig,
|
||||
schema: string,
|
||||
table: string
|
||||
): Promise<TableDefinition> {
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
|
||||
try {
|
||||
// Query columns with full metadata
|
||||
const columnsResult = await client.query(
|
||||
`
|
||||
SELECT
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.udt_name,
|
||||
c.is_nullable,
|
||||
c.column_default,
|
||||
c.ordinal_position,
|
||||
c.character_maximum_length,
|
||||
c.numeric_precision,
|
||||
c.numeric_scale,
|
||||
c.collation_name,
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key,
|
||||
col_description(
|
||||
(quote_ident($1) || '.' || quote_ident($2))::regclass,
|
||||
c.ordinal_position
|
||||
) as column_comment
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN (
|
||||
SELECT kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
WHERE tc.constraint_type = 'PRIMARY KEY'
|
||||
AND tc.table_schema = $1
|
||||
AND tc.table_name = $2
|
||||
) pk ON c.column_name = pk.column_name
|
||||
WHERE c.table_schema = $1 AND c.table_name = $2
|
||||
ORDER BY c.ordinal_position
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
// Query constraints
|
||||
const constraintsResult = await client.query(
|
||||
`
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
tc.constraint_type,
|
||||
kcu.column_name,
|
||||
ccu.table_schema AS ref_schema,
|
||||
ccu.table_name AS ref_table,
|
||||
ccu.column_name AS ref_column,
|
||||
rc.update_rule,
|
||||
rc.delete_rule,
|
||||
cc.check_clause
|
||||
FROM information_schema.table_constraints tc
|
||||
LEFT JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
LEFT JOIN information_schema.constraint_column_usage ccu
|
||||
ON tc.constraint_name = ccu.constraint_name
|
||||
AND tc.constraint_type = 'FOREIGN KEY'
|
||||
LEFT JOIN information_schema.referential_constraints rc
|
||||
ON tc.constraint_name = rc.constraint_name
|
||||
LEFT JOIN information_schema.check_constraints cc
|
||||
ON tc.constraint_name = cc.constraint_name
|
||||
WHERE tc.table_schema = $1 AND tc.table_name = $2
|
||||
ORDER BY tc.constraint_name, kcu.ordinal_position
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
// Query indexes
|
||||
const indexesResult = await client.query(
|
||||
`
|
||||
SELECT
|
||||
i.relname as index_name,
|
||||
ix.indisunique as is_unique,
|
||||
am.amname as index_method,
|
||||
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns,
|
||||
pg_get_expr(ix.indpred, ix.indrelid) as where_clause
|
||||
FROM pg_index ix
|
||||
JOIN pg_class i ON i.oid = ix.indexrelid
|
||||
JOIN pg_class t ON t.oid = ix.indrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
JOIN pg_am am ON am.oid = i.relam
|
||||
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
||||
WHERE n.nspname = $1 AND t.relname = $2
|
||||
AND NOT ix.indisprimary -- Exclude primary key index
|
||||
GROUP BY i.relname, ix.indisunique, am.amname, ix.indpred, ix.indrelid
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
// Query table comment
|
||||
const tableCommentResult = await client.query(
|
||||
`
|
||||
SELECT obj_description(
|
||||
(quote_ident($1) || '.' || quote_ident($2))::regclass
|
||||
) as comment
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
// Build TableDefinition
|
||||
const columns: ColumnDefinition[] = columnsResult.rows.map((row, idx) => ({
|
||||
id: `col-${idx}`,
|
||||
name: row.column_name,
|
||||
dataType: row.udt_name,
|
||||
length: row.character_maximum_length || undefined,
|
||||
precision: row.numeric_precision || undefined,
|
||||
scale: row.numeric_scale || undefined,
|
||||
isNullable: row.is_nullable === 'YES',
|
||||
isPrimaryKey: row.is_primary_key,
|
||||
isUnique: false, // Will be set from constraints
|
||||
defaultValue: row.column_default || undefined,
|
||||
comment: row.column_comment || undefined,
|
||||
collation: row.collation_name || undefined
|
||||
}))
|
||||
|
||||
// Build constraints from query results
|
||||
const constraintMap = new Map<
|
||||
string,
|
||||
{
|
||||
type: string
|
||||
columns: string[]
|
||||
refSchema?: string
|
||||
refTable?: string
|
||||
refColumns?: string[]
|
||||
onUpdate?: string
|
||||
onDelete?: string
|
||||
checkExpression?: string
|
||||
}
|
||||
>()
|
||||
|
||||
for (const row of constraintsResult.rows) {
|
||||
const key = row.constraint_name
|
||||
if (!constraintMap.has(key)) {
|
||||
constraintMap.set(key, {
|
||||
type: row.constraint_type,
|
||||
columns: [],
|
||||
refSchema: row.ref_schema,
|
||||
refTable: row.ref_table,
|
||||
refColumns: [],
|
||||
onUpdate: row.update_rule,
|
||||
onDelete: row.delete_rule,
|
||||
checkExpression: row.check_clause
|
||||
})
|
||||
}
|
||||
const constraint = constraintMap.get(key)!
|
||||
if (row.column_name && !constraint.columns.includes(row.column_name)) {
|
||||
constraint.columns.push(row.column_name)
|
||||
}
|
||||
if (row.ref_column && !constraint.refColumns!.includes(row.ref_column)) {
|
||||
constraint.refColumns!.push(row.ref_column)
|
||||
}
|
||||
}
|
||||
|
||||
const constraints: ConstraintDefinition[] = []
|
||||
let constraintIdx = 0
|
||||
for (const [name, data] of constraintMap) {
|
||||
// Skip primary key (handled at column level)
|
||||
if (data.type === 'PRIMARY KEY') continue
|
||||
|
||||
const constraintDef: ConstraintDefinition = {
|
||||
id: `constraint-${constraintIdx++}`,
|
||||
name,
|
||||
type:
|
||||
data.type === 'FOREIGN KEY'
|
||||
? 'foreign_key'
|
||||
: data.type === 'UNIQUE'
|
||||
? 'unique'
|
||||
: data.type === 'CHECK'
|
||||
? 'check'
|
||||
: 'unique',
|
||||
columns: data.columns
|
||||
}
|
||||
|
||||
if (data.type === 'FOREIGN KEY') {
|
||||
constraintDef.referencedSchema = data.refSchema
|
||||
constraintDef.referencedTable = data.refTable
|
||||
constraintDef.referencedColumns = data.refColumns
|
||||
constraintDef.onUpdate = data.onUpdate as ConstraintDefinition['onUpdate']
|
||||
constraintDef.onDelete = data.onDelete as ConstraintDefinition['onDelete']
|
||||
}
|
||||
|
||||
if (data.type === 'CHECK') {
|
||||
constraintDef.checkExpression = data.checkExpression
|
||||
}
|
||||
|
||||
// Mark columns as unique for UNIQUE constraints
|
||||
if (data.type === 'UNIQUE' && data.columns.length === 1) {
|
||||
const col = columns.find((c) => c.name === data.columns[0])
|
||||
if (col) col.isUnique = true
|
||||
}
|
||||
|
||||
constraints.push(constraintDef)
|
||||
}
|
||||
|
||||
// Build indexes
|
||||
const indexes: IndexDefinition[] = indexesResult.rows.map((row, idx) => {
|
||||
// Handle columns array - could be null, undefined, or not an array in some cases
|
||||
const columnsArray = Array.isArray(row.columns)
|
||||
? row.columns.filter((c: string | null) => c !== null)
|
||||
: []
|
||||
|
||||
return {
|
||||
id: `index-${idx}`,
|
||||
name: row.index_name,
|
||||
columns: columnsArray.map((c: string) => ({ name: c })),
|
||||
isUnique: row.is_unique,
|
||||
method: row.index_method as IndexDefinition['method'],
|
||||
where: row.where_clause || undefined
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
schema,
|
||||
name: table,
|
||||
columns,
|
||||
constraints,
|
||||
indexes,
|
||||
comment: tableCommentResult.rows[0]?.comment || undefined
|
||||
}
|
||||
} finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
async getSequences(config: ConnectionConfig): Promise<SequenceInfo[]> {
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
|
||||
try {
|
||||
const result = await client.query(`
|
||||
SELECT
|
||||
schemaname as schema,
|
||||
sequencename as name,
|
||||
data_type,
|
||||
start_value::text,
|
||||
increment_by::text as increment
|
||||
FROM pg_sequences
|
||||
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
||||
ORDER BY schemaname, sequencename
|
||||
`)
|
||||
|
||||
return result.rows.map((row) => ({
|
||||
schema: row.schema,
|
||||
name: row.name,
|
||||
dataType: row.data_type,
|
||||
startValue: row.start_value,
|
||||
increment: row.increment
|
||||
}))
|
||||
} finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
async getTypes(config: ConnectionConfig): Promise<CustomTypeInfo[]> {
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
|
||||
try {
|
||||
// Get enum types with their values
|
||||
const enumsResult = await client.query(`
|
||||
SELECT
|
||||
n.nspname as schema,
|
||||
t.typname as name,
|
||||
'enum' as type_category,
|
||||
array_agg(e.enumlabel ORDER BY e.enumsortorder) as values
|
||||
FROM pg_type t
|
||||
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||
JOIN pg_enum e ON e.enumtypid = t.oid
|
||||
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
GROUP BY n.nspname, t.typname
|
||||
ORDER BY n.nspname, t.typname
|
||||
`)
|
||||
|
||||
// Get domain types
|
||||
const domainsResult = await client.query(`
|
||||
SELECT
|
||||
n.nspname as schema,
|
||||
t.typname as name,
|
||||
'domain' as type_category
|
||||
FROM pg_type t
|
||||
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||
WHERE t.typtype = 'd'
|
||||
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
ORDER BY n.nspname, t.typname
|
||||
`)
|
||||
|
||||
return [
|
||||
...enumsResult.rows.map((row) => ({
|
||||
schema: row.schema,
|
||||
name: row.name,
|
||||
type: 'enum' as const,
|
||||
values: row.values
|
||||
})),
|
||||
...domainsResult.rows.map((row) => ({
|
||||
schema: row.schema,
|
||||
name: row.name,
|
||||
type: 'domain' as const
|
||||
}))
|
||||
]
|
||||
} finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
102
apps/desktop/src/main/db-adapter.ts
Normal file
102
apps/desktop/src/main/db-adapter.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import type {
|
||||
ConnectionConfig,
|
||||
DatabaseType,
|
||||
SchemaInfo,
|
||||
QueryField,
|
||||
TableDefinition,
|
||||
SequenceInfo,
|
||||
CustomTypeInfo
|
||||
} from '@shared/index'
|
||||
|
||||
/**
|
||||
* Query result with metadata
|
||||
*/
|
||||
export interface AdapterQueryResult {
|
||||
rows: Record<string, unknown>[]
|
||||
fields: QueryField[]
|
||||
rowCount: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Explain plan result
|
||||
*/
|
||||
export interface ExplainResult {
|
||||
plan: unknown
|
||||
durationMs: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Database adapter interface - abstracts database-specific operations
|
||||
*/
|
||||
export interface DatabaseAdapter {
|
||||
/** Database type identifier */
|
||||
readonly dbType: DatabaseType
|
||||
|
||||
/** Test connection */
|
||||
connect(config: ConnectionConfig): Promise<void>
|
||||
|
||||
/** Execute a query and return results */
|
||||
query(config: ConnectionConfig, sql: string): Promise<AdapterQueryResult>
|
||||
|
||||
/** Execute a statement (for INSERT/UPDATE/DELETE in transactions) */
|
||||
execute(
|
||||
config: ConnectionConfig,
|
||||
sql: string,
|
||||
params: unknown[]
|
||||
): Promise<{ rowCount: number | null }>
|
||||
|
||||
/** Execute multiple statements in a transaction */
|
||||
executeTransaction(
|
||||
config: ConnectionConfig,
|
||||
statements: Array<{ sql: string; params: unknown[] }>
|
||||
): Promise<{ rowsAffected: number; results: Array<{ rowCount: number | null }> }>
|
||||
|
||||
/** Fetch database schemas, tables, and columns */
|
||||
getSchemas(config: ConnectionConfig): Promise<SchemaInfo[]>
|
||||
|
||||
/** Get query execution plan */
|
||||
explain(config: ConnectionConfig, sql: string, analyze: boolean): Promise<ExplainResult>
|
||||
|
||||
/** Get table definition (reverse engineer DDL) */
|
||||
getTableDDL(config: ConnectionConfig, schema: string, table: string): Promise<TableDefinition>
|
||||
|
||||
/** Get available sequences (PostgreSQL-specific, returns empty for MySQL) */
|
||||
getSequences(config: ConnectionConfig): Promise<SequenceInfo[]>
|
||||
|
||||
/** Get custom types (enums, etc.) */
|
||||
getTypes(config: ConnectionConfig): Promise<CustomTypeInfo[]>
|
||||
}
|
||||
|
||||
// Import adapters
|
||||
import { PostgresAdapter } from './adapters/postgres-adapter'
|
||||
import { MySQLAdapter } from './adapters/mysql-adapter'
|
||||
|
||||
// Adapter instances (singletons)
|
||||
const adapters: Record<DatabaseType, DatabaseAdapter> = {
|
||||
postgresql: new PostgresAdapter(),
|
||||
mysql: new MySQLAdapter(),
|
||||
sqlite: new PostgresAdapter() // Placeholder - SQLite not implemented yet
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate database adapter for a connection
|
||||
*/
|
||||
export function getAdapter(config: ConnectionConfig): DatabaseAdapter {
|
||||
const dbType = config.dbType || 'postgresql' // Default to postgresql for backward compatibility
|
||||
const adapter = adapters[dbType]
|
||||
if (!adapter) {
|
||||
throw new Error(`Unsupported database type: ${dbType}`)
|
||||
}
|
||||
return adapter
|
||||
}
|
||||
|
||||
/**
|
||||
* Get adapter by database type
|
||||
*/
|
||||
export function getAdapterByType(dbType: DatabaseType): DatabaseAdapter {
|
||||
const adapter = adapters[dbType]
|
||||
if (!adapter) {
|
||||
throw new Error(`Unsupported database type: ${dbType}`)
|
||||
}
|
||||
return adapter
|
||||
}
|
||||
|
|
@ -1,25 +1,14 @@
|
|||
import { app, shell, BrowserWindow, ipcMain } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||
import { Client } from 'pg'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
import type {
|
||||
ConnectionConfig,
|
||||
SchemaInfo,
|
||||
TableInfo,
|
||||
ColumnInfo,
|
||||
QueryField,
|
||||
ForeignKeyInfo,
|
||||
EditBatch,
|
||||
EditResult,
|
||||
TableDefinition,
|
||||
AlterTableBatch,
|
||||
DDLResult,
|
||||
SequenceInfo,
|
||||
CustomTypeInfo,
|
||||
ColumnDefinition,
|
||||
ConstraintDefinition,
|
||||
IndexDefinition
|
||||
DDLResult
|
||||
} from '@shared/index'
|
||||
import { buildQuery, validateOperation, buildPreviewSql } from './sql-builder'
|
||||
import {
|
||||
|
|
@ -32,83 +21,7 @@ import {
|
|||
import { createMenu } from './menu'
|
||||
import { setupContextMenu } from './context-menu'
|
||||
import { getWindowState, trackWindowState } from './window-state'
|
||||
|
||||
// ============================================
|
||||
// PostgreSQL OID to Type Name Mapping
|
||||
// Reference: https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.dat
|
||||
// ============================================
|
||||
const PG_TYPE_MAP: Record<number, string> = {
|
||||
16: 'boolean',
|
||||
17: 'bytea',
|
||||
18: 'char',
|
||||
19: 'name',
|
||||
20: 'bigint',
|
||||
21: 'smallint',
|
||||
23: 'integer',
|
||||
24: 'regproc',
|
||||
25: 'text',
|
||||
26: 'oid',
|
||||
114: 'json',
|
||||
142: 'xml',
|
||||
600: 'point',
|
||||
601: 'lseg',
|
||||
602: 'path',
|
||||
603: 'box',
|
||||
604: 'polygon',
|
||||
628: 'line',
|
||||
700: 'real',
|
||||
701: 'double precision',
|
||||
718: 'circle',
|
||||
790: 'money',
|
||||
829: 'macaddr',
|
||||
869: 'inet',
|
||||
650: 'cidr',
|
||||
1042: 'char',
|
||||
1043: 'varchar',
|
||||
1082: 'date',
|
||||
1083: 'time',
|
||||
1114: 'timestamp',
|
||||
1184: 'timestamptz',
|
||||
1186: 'interval',
|
||||
1266: 'timetz',
|
||||
1560: 'bit',
|
||||
1562: 'varbit',
|
||||
1700: 'numeric',
|
||||
2950: 'uuid',
|
||||
3802: 'jsonb',
|
||||
3904: 'int4range',
|
||||
3906: 'numrange',
|
||||
3908: 'tsrange',
|
||||
3910: 'tstzrange',
|
||||
3912: 'daterange',
|
||||
3926: 'int8range',
|
||||
// Array types (common ones)
|
||||
1000: 'boolean[]',
|
||||
1001: 'bytea[]',
|
||||
1005: 'smallint[]',
|
||||
1007: 'integer[]',
|
||||
1009: 'text[]',
|
||||
1014: 'char[]',
|
||||
1015: 'varchar[]',
|
||||
1016: 'bigint[]',
|
||||
1021: 'real[]',
|
||||
1022: 'double precision[]',
|
||||
1028: 'oid[]',
|
||||
1115: 'timestamp[]',
|
||||
1182: 'date[]',
|
||||
1183: 'time[]',
|
||||
1231: 'numeric[]',
|
||||
2951: 'uuid[]',
|
||||
3807: 'jsonb[]',
|
||||
199: 'json[]'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve PostgreSQL OID to human-readable type name
|
||||
*/
|
||||
function resolvePostgresType(dataTypeID: number): string {
|
||||
return PG_TYPE_MAP[dataTypeID] ?? `unknown(${dataTypeID})`
|
||||
}
|
||||
import { getAdapter } from './db-adapter'
|
||||
|
||||
// electron-store v11 is ESM-only, use dynamic import
|
||||
type StoreType = import('electron-store').default<{ connections: ConnectionConfig[] }>
|
||||
|
|
@ -224,11 +137,10 @@ app.whenReady().then(async () => {
|
|||
})
|
||||
|
||||
// IPC Handlers
|
||||
ipcMain.handle('db:connect', async (_, config) => {
|
||||
ipcMain.handle('db:connect', async (_, config: ConnectionConfig) => {
|
||||
try {
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
await client.end()
|
||||
const adapter = getAdapter(config)
|
||||
await adapter.connect(config)
|
||||
return { success: true }
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
|
@ -236,36 +148,26 @@ app.whenReady().then(async () => {
|
|||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('db:query', async (_, { config, query }) => {
|
||||
ipcMain.handle('db:query', async (_, { config, query }: { config: ConnectionConfig; query: string }) => {
|
||||
console.log('[main:db:query] Received query request')
|
||||
console.log('[main:db:query] Config:', { ...config, password: '***' })
|
||||
console.log('[main:db:query] Query:', query)
|
||||
|
||||
try {
|
||||
const client = new Client(config)
|
||||
const adapter = getAdapter(config)
|
||||
console.log('[main:db:query] Connecting...')
|
||||
await client.connect()
|
||||
console.log('[main:db:query] Connected, executing query...')
|
||||
const start = Date.now()
|
||||
const res = await client.query(query)
|
||||
const result = await adapter.query(config, query)
|
||||
const duration = Date.now() - start
|
||||
console.log('[main:db:query] Query completed in', duration, 'ms')
|
||||
console.log('[main:db:query] Rows:', res.rowCount)
|
||||
await client.end()
|
||||
|
||||
// Map fields with resolved type names
|
||||
const fields: QueryField[] = res.fields.map((f) => ({
|
||||
name: f.name,
|
||||
dataType: resolvePostgresType(f.dataTypeID),
|
||||
dataTypeID: f.dataTypeID
|
||||
}))
|
||||
console.log('[main:db:query] Rows:', result.rowCount)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
rows: res.rows,
|
||||
fields,
|
||||
rowCount: res.rowCount,
|
||||
rows: result.rows,
|
||||
fields: result.fields,
|
||||
rowCount: result.rowCount,
|
||||
durationMs: duration
|
||||
}
|
||||
}
|
||||
|
|
@ -278,160 +180,9 @@ app.whenReady().then(async () => {
|
|||
|
||||
// Fetch database schemas, tables, and columns
|
||||
ipcMain.handle('db:schemas', async (_, config: ConnectionConfig) => {
|
||||
const client = new Client(config)
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
|
||||
// Query 1: Get all schemas (excluding system schemas)
|
||||
const schemasResult = await client.query(`
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
||||
ORDER BY schema_name
|
||||
`)
|
||||
|
||||
// Query 2: Get all tables and views
|
||||
const tablesResult = await client.query(`
|
||||
SELECT
|
||||
table_schema,
|
||||
table_name,
|
||||
table_type
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
||||
ORDER BY table_schema, table_name
|
||||
`)
|
||||
|
||||
// Query 3: Get all columns with primary key info
|
||||
const columnsResult = await client.query(`
|
||||
SELECT
|
||||
c.table_schema,
|
||||
c.table_name,
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.udt_name,
|
||||
c.is_nullable,
|
||||
c.column_default,
|
||||
c.ordinal_position,
|
||||
c.character_maximum_length,
|
||||
c.numeric_precision,
|
||||
c.numeric_scale,
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
kcu.table_schema,
|
||||
kcu.table_name,
|
||||
kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
WHERE tc.constraint_type = 'PRIMARY KEY'
|
||||
) pk ON c.table_schema = pk.table_schema
|
||||
AND c.table_name = pk.table_name
|
||||
AND c.column_name = pk.column_name
|
||||
WHERE c.table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
||||
ORDER BY c.table_schema, c.table_name, c.ordinal_position
|
||||
`)
|
||||
|
||||
// Query 4: Get all foreign key relationships
|
||||
const foreignKeysResult = await client.query(`
|
||||
SELECT
|
||||
tc.table_schema,
|
||||
tc.table_name,
|
||||
kcu.column_name,
|
||||
tc.constraint_name,
|
||||
ccu.table_schema AS referenced_schema,
|
||||
ccu.table_name AS referenced_table,
|
||||
ccu.column_name AS referenced_column
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.constraint_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
||||
ORDER BY tc.table_schema, tc.table_name, kcu.column_name
|
||||
`)
|
||||
|
||||
await client.end()
|
||||
|
||||
// Build foreign key lookup map: "schema.table.column" -> ForeignKeyInfo
|
||||
const fkMap = new Map<string, ForeignKeyInfo>()
|
||||
for (const row of foreignKeysResult.rows) {
|
||||
const key = `${row.table_schema}.${row.table_name}.${row.column_name}`
|
||||
fkMap.set(key, {
|
||||
constraintName: row.constraint_name,
|
||||
referencedSchema: row.referenced_schema,
|
||||
referencedTable: row.referenced_table,
|
||||
referencedColumn: row.referenced_column
|
||||
})
|
||||
}
|
||||
|
||||
// Build schema structure
|
||||
const schemaMap = new Map<string, SchemaInfo>()
|
||||
|
||||
// Initialize schemas
|
||||
for (const row of schemasResult.rows) {
|
||||
schemaMap.set(row.schema_name, {
|
||||
name: row.schema_name,
|
||||
tables: []
|
||||
})
|
||||
}
|
||||
|
||||
// Build tables map for easy column assignment
|
||||
const tableMap = new Map<string, TableInfo>()
|
||||
for (const row of tablesResult.rows) {
|
||||
const tableKey = `${row.table_schema}.${row.table_name}`
|
||||
const table: TableInfo = {
|
||||
name: row.table_name,
|
||||
type: row.table_type === 'VIEW' ? 'view' : 'table',
|
||||
columns: []
|
||||
}
|
||||
tableMap.set(tableKey, table)
|
||||
|
||||
// Add table to its schema
|
||||
const schema = schemaMap.get(row.table_schema)
|
||||
if (schema) {
|
||||
schema.tables.push(table)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign columns to tables
|
||||
for (const row of columnsResult.rows) {
|
||||
const tableKey = `${row.table_schema}.${row.table_name}`
|
||||
const table = tableMap.get(tableKey)
|
||||
if (table) {
|
||||
// Format data type nicely
|
||||
let dataType = row.udt_name
|
||||
if (row.character_maximum_length) {
|
||||
dataType = `${row.udt_name}(${row.character_maximum_length})`
|
||||
} else if (row.numeric_precision && row.numeric_scale) {
|
||||
dataType = `${row.udt_name}(${row.numeric_precision},${row.numeric_scale})`
|
||||
}
|
||||
|
||||
// Check for foreign key relationship
|
||||
const fkKey = `${row.table_schema}.${row.table_name}.${row.column_name}`
|
||||
const foreignKey = fkMap.get(fkKey)
|
||||
|
||||
const column: ColumnInfo = {
|
||||
name: row.column_name,
|
||||
dataType,
|
||||
isNullable: row.is_nullable === 'YES',
|
||||
isPrimaryKey: row.is_primary_key,
|
||||
defaultValue: row.column_default || undefined,
|
||||
ordinalPosition: row.ordinal_position,
|
||||
foreignKey
|
||||
}
|
||||
table.columns.push(column)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to array
|
||||
const schemas = Array.from(schemaMap.values())
|
||||
const adapter = getAdapter(config)
|
||||
const schemas = await adapter.getSchemas(config)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -441,7 +192,6 @@ app.whenReady().then(async () => {
|
|||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
await client.end().catch(() => {})
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
|
|
@ -506,7 +256,8 @@ app.whenReady().then(async () => {
|
|||
console.log('[main:db:execute] Context:', batch.context)
|
||||
console.log('[main:db:execute] Operations count:', batch.operations.length)
|
||||
|
||||
const client = new Client(config)
|
||||
const adapter = getAdapter(config)
|
||||
const dbType = config.dbType || 'postgresql'
|
||||
const result: EditResult = {
|
||||
success: true,
|
||||
rowsAffected: 0,
|
||||
|
|
@ -514,69 +265,60 @@ app.whenReady().then(async () => {
|
|||
errors: []
|
||||
}
|
||||
|
||||
// Validate operations first
|
||||
const validOperations: Array<{ sql: string; params: unknown[]; preview: string; opId: string }> = []
|
||||
for (const operation of batch.operations) {
|
||||
const validation = validateOperation(operation)
|
||||
if (!validation.valid) {
|
||||
result.errors!.push({
|
||||
operationId: operation.id,
|
||||
message: validation.error!
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const query = buildQuery(operation, batch.context, dbType)
|
||||
const previewSql = buildPreviewSql(operation, batch.context, dbType)
|
||||
validOperations.push({ sql: query.sql, params: query.params, preview: previewSql, opId: operation.id })
|
||||
}
|
||||
|
||||
// If all operations have validation errors, return early
|
||||
if (validOperations.length === 0 && result.errors!.length > 0) {
|
||||
result.success = false
|
||||
return { success: true, data: result }
|
||||
}
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
const statements = validOperations.map((op) => ({ sql: op.sql, params: op.params }))
|
||||
const txResult = await adapter.executeTransaction(config, statements)
|
||||
|
||||
// Start transaction for atomicity
|
||||
await client.query('BEGIN')
|
||||
result.rowsAffected = txResult.rowsAffected
|
||||
result.executedSql = validOperations.map((op) => op.preview)
|
||||
|
||||
for (const operation of batch.operations) {
|
||||
// Validate operation
|
||||
const validation = validateOperation(operation)
|
||||
if (!validation.valid) {
|
||||
result.errors!.push({
|
||||
operationId: operation.id,
|
||||
message: validation.error!
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
// Build parameterized query
|
||||
const query = buildQuery(operation, batch.context, 'postgresql')
|
||||
const previewSql = buildPreviewSql(operation, batch.context, 'postgresql')
|
||||
result.executedSql.push(previewSql)
|
||||
|
||||
console.log('[main:db:execute] Executing:', query.sql)
|
||||
console.log('[main:db:execute] Params:', query.params)
|
||||
|
||||
const res = await client.query(query.sql, query.params)
|
||||
result.rowsAffected += res.rowCount ?? 0
|
||||
} catch (opError: unknown) {
|
||||
const errorMessage = opError instanceof Error ? opError.message : String(opError)
|
||||
result.errors!.push({
|
||||
operationId: operation.id,
|
||||
message: errorMessage
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If any errors, rollback; otherwise commit
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
await client.query('ROLLBACK')
|
||||
result.success = false
|
||||
} else {
|
||||
await client.query('COMMIT')
|
||||
}
|
||||
|
||||
await client.end()
|
||||
return { success: true, data: result }
|
||||
} catch (error: unknown) {
|
||||
console.error('[main:db:execute] Error:', error)
|
||||
await client.query('ROLLBACK').catch(() => {})
|
||||
await client.end().catch(() => {})
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: errorMessage }
|
||||
// Mark all valid operations as failed
|
||||
for (const op of validOperations) {
|
||||
result.errors!.push({
|
||||
operationId: op.opId,
|
||||
message: errorMessage
|
||||
})
|
||||
}
|
||||
result.success = false
|
||||
return { success: true, data: result }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Preview SQL for edit operations (without executing)
|
||||
ipcMain.handle('db:preview-sql', (_, { batch }: { batch: EditBatch }) => {
|
||||
ipcMain.handle('db:preview-sql', (_, { batch, dbType }: { batch: EditBatch; dbType?: string }) => {
|
||||
try {
|
||||
const targetDbType = (dbType || 'postgresql') as 'postgresql' | 'mysql' | 'sqlite'
|
||||
const previews = batch.operations.map((op) => ({
|
||||
operationId: op.id,
|
||||
sql: buildPreviewSql(op, batch.context, 'postgresql')
|
||||
sql: buildPreviewSql(op, batch.context, targetDbType)
|
||||
}))
|
||||
return { success: true, data: previews }
|
||||
} catch (error: unknown) {
|
||||
|
|
@ -586,37 +328,18 @@ app.whenReady().then(async () => {
|
|||
})
|
||||
|
||||
// Execute EXPLAIN ANALYZE for query plan analysis
|
||||
ipcMain.handle('db:explain', async (_, { config, query, analyze }) => {
|
||||
ipcMain.handle('db:explain', async (_, { config, query, analyze }: { config: ConnectionConfig; query: string; analyze: boolean }) => {
|
||||
console.log('[main:db:explain] Received explain request')
|
||||
console.log('[main:db:explain] Query:', query)
|
||||
console.log('[main:db:explain] Analyze:', analyze)
|
||||
|
||||
try {
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
|
||||
// Build EXPLAIN query with JSON format for easy parsing
|
||||
const explainOptions = analyze
|
||||
? 'ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON'
|
||||
: 'COSTS, VERBOSE, FORMAT JSON'
|
||||
const explainQuery = `EXPLAIN (${explainOptions}) ${query}`
|
||||
|
||||
console.log('[main:db:explain] Running:', explainQuery)
|
||||
const start = Date.now()
|
||||
const res = await client.query(explainQuery)
|
||||
const duration = Date.now() - start
|
||||
|
||||
await client.end()
|
||||
|
||||
// EXPLAIN with JSON format returns a single row with "QUERY PLAN" column containing JSON array
|
||||
const planJson = res.rows[0]?.['QUERY PLAN']
|
||||
const adapter = getAdapter(config)
|
||||
const result = await adapter.explain(config, query, analyze)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
plan: planJson,
|
||||
durationMs: duration
|
||||
}
|
||||
data: result
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('[main:db:explain] Error:', error)
|
||||
|
|
@ -647,7 +370,8 @@ app.whenReady().then(async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const client = new Client(config)
|
||||
const adapter = getAdapter(config)
|
||||
const dbType = config.dbType || 'postgresql'
|
||||
const result: DDLResult = {
|
||||
success: true,
|
||||
executedSql: [],
|
||||
|
|
@ -655,31 +379,26 @@ app.whenReady().then(async () => {
|
|||
}
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
await client.query('BEGIN')
|
||||
|
||||
// Build and execute CREATE TABLE statement
|
||||
const { sql } = buildCreateTable(definition, 'postgresql')
|
||||
// Build CREATE TABLE statement
|
||||
const { sql } = buildCreateTable(definition, dbType)
|
||||
result.executedSql.push(sql)
|
||||
|
||||
console.log('[main:db:create-table] Executing:', sql)
|
||||
|
||||
// Execute each statement separately (CREATE TABLE, COMMENT, indexes)
|
||||
const statements = sql.split(/;\s*\n\n/).filter((s) => s.trim())
|
||||
for (const stmt of statements) {
|
||||
if (stmt.trim()) {
|
||||
await client.query(stmt.trim().endsWith(';') ? stmt : stmt + ';')
|
||||
}
|
||||
}
|
||||
const stmtParams = statements
|
||||
.filter((s) => s.trim())
|
||||
.map((stmt) => ({
|
||||
sql: stmt.trim().endsWith(';') ? stmt : stmt + ';',
|
||||
params: []
|
||||
}))
|
||||
|
||||
await client.query('COMMIT')
|
||||
await client.end()
|
||||
await adapter.executeTransaction(config, stmtParams)
|
||||
|
||||
return { success: true, data: result }
|
||||
} catch (error: unknown) {
|
||||
console.error('[main:db:create-table] Error:', error)
|
||||
await client.query('ROLLBACK').catch(() => {})
|
||||
await client.end().catch(() => {})
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
|
|
@ -692,7 +411,8 @@ app.whenReady().then(async () => {
|
|||
async (_, { config, batch }: { config: ConnectionConfig; batch: AlterTableBatch }) => {
|
||||
console.log('[main:db:alter-table] Altering table:', batch.schema, batch.table)
|
||||
|
||||
const client = new Client(config)
|
||||
const adapter = getAdapter(config)
|
||||
const dbType = config.dbType || 'postgresql'
|
||||
const result: DDLResult = {
|
||||
success: true,
|
||||
executedSql: [],
|
||||
|
|
@ -700,39 +420,22 @@ app.whenReady().then(async () => {
|
|||
}
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
await client.query('BEGIN')
|
||||
// Build ALTER TABLE statements
|
||||
const queries = buildAlterTable(batch, dbType)
|
||||
const statements = queries.map((q) => ({ sql: q.sql, params: [] }))
|
||||
result.executedSql = queries.map((q) => q.sql)
|
||||
|
||||
// Build and execute ALTER TABLE statements
|
||||
const queries = buildAlterTable(batch, 'postgresql')
|
||||
console.log('[main:db:alter-table] Executing:', result.executedSql)
|
||||
|
||||
for (const query of queries) {
|
||||
result.executedSql.push(query.sql)
|
||||
console.log('[main:db:alter-table] Executing:', query.sql)
|
||||
await adapter.executeTransaction(config, statements)
|
||||
|
||||
try {
|
||||
await client.query(query.sql)
|
||||
} catch (opError: unknown) {
|
||||
const errorMessage = opError instanceof Error ? opError.message : String(opError)
|
||||
result.errors!.push(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
await client.query('ROLLBACK')
|
||||
result.success = false
|
||||
} else {
|
||||
await client.query('COMMIT')
|
||||
}
|
||||
|
||||
await client.end()
|
||||
return { success: true, data: result }
|
||||
} catch (error: unknown) {
|
||||
console.error('[main:db:alter-table] Error:', error)
|
||||
await client.query('ROLLBACK').catch(() => {})
|
||||
await client.end().catch(() => {})
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: errorMessage }
|
||||
result.errors!.push(errorMessage)
|
||||
result.success = false
|
||||
return { success: true, data: result }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -751,16 +454,14 @@ app.whenReady().then(async () => {
|
|||
) => {
|
||||
console.log('[main:db:drop-table] Dropping table:', schema, table)
|
||||
|
||||
const client = new Client(config)
|
||||
const adapter = getAdapter(config)
|
||||
const dbType = config.dbType || 'postgresql'
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
|
||||
const { sql } = buildDropTable(schema, table, cascade, 'postgresql')
|
||||
const { sql } = buildDropTable(schema, table, cascade, dbType)
|
||||
console.log('[main:db:drop-table] Executing:', sql)
|
||||
|
||||
await client.query(sql)
|
||||
await client.end()
|
||||
await adapter.executeTransaction(config, [{ sql, params: [] }])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -768,7 +469,6 @@ app.whenReady().then(async () => {
|
|||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('[main:db:drop-table] Error:', error)
|
||||
await client.end().catch(() => {})
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
|
|
@ -784,227 +484,12 @@ app.whenReady().then(async () => {
|
|||
) => {
|
||||
console.log('[main:db:get-table-ddl] Getting DDL for:', schema, table)
|
||||
|
||||
const client = new Client(config)
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
|
||||
// Query columns with full metadata
|
||||
const columnsResult = await client.query(
|
||||
`
|
||||
SELECT
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.udt_name,
|
||||
c.is_nullable,
|
||||
c.column_default,
|
||||
c.ordinal_position,
|
||||
c.character_maximum_length,
|
||||
c.numeric_precision,
|
||||
c.numeric_scale,
|
||||
c.collation_name,
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key,
|
||||
col_description(
|
||||
(quote_ident($1) || '.' || quote_ident($2))::regclass,
|
||||
c.ordinal_position
|
||||
) as column_comment
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN (
|
||||
SELECT kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
WHERE tc.constraint_type = 'PRIMARY KEY'
|
||||
AND tc.table_schema = $1
|
||||
AND tc.table_name = $2
|
||||
) pk ON c.column_name = pk.column_name
|
||||
WHERE c.table_schema = $1 AND c.table_name = $2
|
||||
ORDER BY c.ordinal_position
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
// Query constraints
|
||||
const constraintsResult = await client.query(
|
||||
`
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
tc.constraint_type,
|
||||
kcu.column_name,
|
||||
ccu.table_schema AS ref_schema,
|
||||
ccu.table_name AS ref_table,
|
||||
ccu.column_name AS ref_column,
|
||||
rc.update_rule,
|
||||
rc.delete_rule,
|
||||
cc.check_clause
|
||||
FROM information_schema.table_constraints tc
|
||||
LEFT JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
LEFT JOIN information_schema.constraint_column_usage ccu
|
||||
ON tc.constraint_name = ccu.constraint_name
|
||||
AND tc.constraint_type = 'FOREIGN KEY'
|
||||
LEFT JOIN information_schema.referential_constraints rc
|
||||
ON tc.constraint_name = rc.constraint_name
|
||||
LEFT JOIN information_schema.check_constraints cc
|
||||
ON tc.constraint_name = cc.constraint_name
|
||||
WHERE tc.table_schema = $1 AND tc.table_name = $2
|
||||
ORDER BY tc.constraint_name, kcu.ordinal_position
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
// Query indexes
|
||||
const indexesResult = await client.query(
|
||||
`
|
||||
SELECT
|
||||
i.relname as index_name,
|
||||
ix.indisunique as is_unique,
|
||||
am.amname as index_method,
|
||||
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns,
|
||||
pg_get_expr(ix.indpred, ix.indrelid) as where_clause
|
||||
FROM pg_index ix
|
||||
JOIN pg_class i ON i.oid = ix.indexrelid
|
||||
JOIN pg_class t ON t.oid = ix.indrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
JOIN pg_am am ON am.oid = i.relam
|
||||
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
||||
WHERE n.nspname = $1 AND t.relname = $2
|
||||
AND NOT ix.indisprimary -- Exclude primary key index
|
||||
GROUP BY i.relname, ix.indisunique, am.amname, ix.indpred, ix.indrelid
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
// Query table comment
|
||||
const tableCommentResult = await client.query(
|
||||
`
|
||||
SELECT obj_description(
|
||||
(quote_ident($1) || '.' || quote_ident($2))::regclass
|
||||
) as comment
|
||||
`,
|
||||
[schema, table]
|
||||
)
|
||||
|
||||
await client.end()
|
||||
|
||||
// Build TableDefinition
|
||||
const columns: ColumnDefinition[] = columnsResult.rows.map((row, idx) => ({
|
||||
id: `col-${idx}`,
|
||||
name: row.column_name,
|
||||
dataType: row.udt_name,
|
||||
length: row.character_maximum_length || undefined,
|
||||
precision: row.numeric_precision || undefined,
|
||||
scale: row.numeric_scale || undefined,
|
||||
isNullable: row.is_nullable === 'YES',
|
||||
isPrimaryKey: row.is_primary_key,
|
||||
isUnique: false, // Will be set from constraints
|
||||
defaultValue: row.column_default || undefined,
|
||||
comment: row.column_comment || undefined,
|
||||
collation: row.collation_name || undefined
|
||||
}))
|
||||
|
||||
// Build constraints from query results
|
||||
const constraintMap = new Map<
|
||||
string,
|
||||
{ type: string; columns: string[]; refSchema?: string; refTable?: string; refColumns?: string[]; onUpdate?: string; onDelete?: string; checkExpression?: string }
|
||||
>()
|
||||
|
||||
for (const row of constraintsResult.rows) {
|
||||
const key = row.constraint_name
|
||||
if (!constraintMap.has(key)) {
|
||||
constraintMap.set(key, {
|
||||
type: row.constraint_type,
|
||||
columns: [],
|
||||
refSchema: row.ref_schema,
|
||||
refTable: row.ref_table,
|
||||
refColumns: [],
|
||||
onUpdate: row.update_rule,
|
||||
onDelete: row.delete_rule,
|
||||
checkExpression: row.check_clause
|
||||
})
|
||||
}
|
||||
const constraint = constraintMap.get(key)!
|
||||
if (row.column_name && !constraint.columns.includes(row.column_name)) {
|
||||
constraint.columns.push(row.column_name)
|
||||
}
|
||||
if (row.ref_column && !constraint.refColumns!.includes(row.ref_column)) {
|
||||
constraint.refColumns!.push(row.ref_column)
|
||||
}
|
||||
}
|
||||
|
||||
const constraints: ConstraintDefinition[] = []
|
||||
let constraintIdx = 0
|
||||
for (const [name, data] of constraintMap) {
|
||||
// Skip primary key (handled at column level)
|
||||
if (data.type === 'PRIMARY KEY') continue
|
||||
|
||||
const constraintDef: ConstraintDefinition = {
|
||||
id: `constraint-${constraintIdx++}`,
|
||||
name,
|
||||
type:
|
||||
data.type === 'FOREIGN KEY'
|
||||
? 'foreign_key'
|
||||
: data.type === 'UNIQUE'
|
||||
? 'unique'
|
||||
: data.type === 'CHECK'
|
||||
? 'check'
|
||||
: 'unique',
|
||||
columns: data.columns
|
||||
}
|
||||
|
||||
if (data.type === 'FOREIGN KEY') {
|
||||
constraintDef.referencedSchema = data.refSchema
|
||||
constraintDef.referencedTable = data.refTable
|
||||
constraintDef.referencedColumns = data.refColumns
|
||||
constraintDef.onUpdate = data.onUpdate as ConstraintDefinition['onUpdate']
|
||||
constraintDef.onDelete = data.onDelete as ConstraintDefinition['onDelete']
|
||||
}
|
||||
|
||||
if (data.type === 'CHECK') {
|
||||
constraintDef.checkExpression = data.checkExpression
|
||||
}
|
||||
|
||||
// Mark columns as unique for UNIQUE constraints
|
||||
if (data.type === 'UNIQUE' && data.columns.length === 1) {
|
||||
const col = columns.find((c) => c.name === data.columns[0])
|
||||
if (col) col.isUnique = true
|
||||
}
|
||||
|
||||
constraints.push(constraintDef)
|
||||
}
|
||||
|
||||
// Build indexes
|
||||
const indexes: IndexDefinition[] = indexesResult.rows.map((row, idx) => {
|
||||
// Handle columns array - could be null, undefined, or not an array in some cases
|
||||
const columnsArray = Array.isArray(row.columns)
|
||||
? row.columns.filter((c: string | null) => c !== null)
|
||||
: []
|
||||
|
||||
return {
|
||||
id: `index-${idx}`,
|
||||
name: row.index_name,
|
||||
columns: columnsArray.map((c: string) => ({ name: c })),
|
||||
isUnique: row.is_unique,
|
||||
method: row.index_method as IndexDefinition['method'],
|
||||
where: row.where_clause || undefined
|
||||
}
|
||||
})
|
||||
|
||||
const definition: TableDefinition = {
|
||||
schema,
|
||||
name: table,
|
||||
columns,
|
||||
constraints,
|
||||
indexes,
|
||||
comment: tableCommentResult.rows[0]?.comment || undefined
|
||||
}
|
||||
|
||||
const adapter = getAdapter(config)
|
||||
const definition = await adapter.getTableDDL(config, schema, table)
|
||||
return { success: true, data: definition }
|
||||
} catch (error: unknown) {
|
||||
console.error('[main:db:get-table-ddl] Error:', error)
|
||||
await client.end().catch(() => {})
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
|
|
@ -1015,37 +500,12 @@ app.whenReady().then(async () => {
|
|||
ipcMain.handle('db:get-sequences', async (_, config: ConnectionConfig) => {
|
||||
console.log('[main:db:get-sequences] Fetching sequences')
|
||||
|
||||
const client = new Client(config)
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
|
||||
const result = await client.query(`
|
||||
SELECT
|
||||
schemaname as schema,
|
||||
sequencename as name,
|
||||
data_type,
|
||||
start_value::text,
|
||||
increment_by::text as increment
|
||||
FROM pg_sequences
|
||||
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
||||
ORDER BY schemaname, sequencename
|
||||
`)
|
||||
|
||||
await client.end()
|
||||
|
||||
const sequences: SequenceInfo[] = result.rows.map((row) => ({
|
||||
schema: row.schema,
|
||||
name: row.name,
|
||||
dataType: row.data_type,
|
||||
startValue: row.start_value,
|
||||
increment: row.increment
|
||||
}))
|
||||
|
||||
const adapter = getAdapter(config)
|
||||
const sequences = await adapter.getSequences(config)
|
||||
return { success: true, data: sequences }
|
||||
} catch (error: unknown) {
|
||||
console.error('[main:db:get-sequences] Error:', error)
|
||||
await client.end().catch(() => {})
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
|
|
@ -1055,68 +515,22 @@ app.whenReady().then(async () => {
|
|||
ipcMain.handle('db:get-types', async (_, config: ConnectionConfig) => {
|
||||
console.log('[main:db:get-types] Fetching custom types')
|
||||
|
||||
const client = new Client(config)
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
|
||||
// Get enum types with their values
|
||||
const enumsResult = await client.query(`
|
||||
SELECT
|
||||
n.nspname as schema,
|
||||
t.typname as name,
|
||||
'enum' as type_category,
|
||||
array_agg(e.enumlabel ORDER BY e.enumsortorder) as values
|
||||
FROM pg_type t
|
||||
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||
JOIN pg_enum e ON e.enumtypid = t.oid
|
||||
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
GROUP BY n.nspname, t.typname
|
||||
ORDER BY n.nspname, t.typname
|
||||
`)
|
||||
|
||||
// Get domain types
|
||||
const domainsResult = await client.query(`
|
||||
SELECT
|
||||
n.nspname as schema,
|
||||
t.typname as name,
|
||||
'domain' as type_category
|
||||
FROM pg_type t
|
||||
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||
WHERE t.typtype = 'd'
|
||||
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
ORDER BY n.nspname, t.typname
|
||||
`)
|
||||
|
||||
await client.end()
|
||||
|
||||
const types: CustomTypeInfo[] = [
|
||||
...enumsResult.rows.map((row) => ({
|
||||
schema: row.schema,
|
||||
name: row.name,
|
||||
type: 'enum' as const,
|
||||
values: row.values
|
||||
})),
|
||||
...domainsResult.rows.map((row) => ({
|
||||
schema: row.schema,
|
||||
name: row.name,
|
||||
type: 'domain' as const
|
||||
}))
|
||||
]
|
||||
|
||||
const adapter = getAdapter(config)
|
||||
const types = await adapter.getTypes(config)
|
||||
return { success: true, data: types }
|
||||
} catch (error: unknown) {
|
||||
console.error('[main:db:get-types] Error:', error)
|
||||
await client.end().catch(() => {})
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
})
|
||||
|
||||
// Preview DDL without executing
|
||||
ipcMain.handle('db:preview-ddl', (_, { definition }: { definition: TableDefinition }) => {
|
||||
ipcMain.handle('db:preview-ddl', (_, { definition, dbType }: { definition: TableDefinition; dbType?: string }) => {
|
||||
try {
|
||||
const sql = buildPreviewDDL(definition, 'postgresql')
|
||||
const targetDbType = (dbType || 'postgresql') as 'postgresql' | 'mysql' | 'sqlite'
|
||||
const sql = buildPreviewDDL(definition, targetDbType)
|
||||
return { success: true, data: sql }
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Loader2, Database, CheckCircle2, XCircle, Link, Settings2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
|
@ -12,16 +12,33 @@ import {
|
|||
SheetHeader,
|
||||
SheetTitle
|
||||
} from '@/components/ui/sheet'
|
||||
import { useConnectionStore } from '@/stores'
|
||||
import { useConnectionStore, type Connection } from '@/stores'
|
||||
import type { DatabaseType } from '@shared/index'
|
||||
|
||||
interface AddConnectionDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
connection?: Connection | null
|
||||
}
|
||||
|
||||
type InputMode = 'manual' | 'connection-string'
|
||||
|
||||
function parseConnectionString(connectionString: string): {
|
||||
const DB_DEFAULTS: Record<DatabaseType, { port: string; user: string; database: string }> = {
|
||||
postgresql: { port: '5432', user: 'postgres', database: 'postgres' },
|
||||
mysql: { port: '3306', user: 'root', database: '' },
|
||||
sqlite: { port: '', user: '', database: '' }
|
||||
}
|
||||
|
||||
const DB_PROTOCOLS: Record<DatabaseType, string[]> = {
|
||||
postgresql: ['postgres', 'postgresql'],
|
||||
mysql: ['mysql'],
|
||||
sqlite: []
|
||||
}
|
||||
|
||||
function parseConnectionString(
|
||||
connectionString: string,
|
||||
dbType: DatabaseType
|
||||
): {
|
||||
host: string
|
||||
port: string
|
||||
database: string
|
||||
|
|
@ -30,17 +47,20 @@ function parseConnectionString(connectionString: string): {
|
|||
ssl: boolean
|
||||
} | null {
|
||||
try {
|
||||
// Handle postgresql:// or postgres:// URLs
|
||||
const url = new URL(connectionString)
|
||||
const protocol = url.protocol.replace(':', '')
|
||||
|
||||
if (!url.protocol.startsWith('postgres')) {
|
||||
// Validate protocol matches db type
|
||||
const validProtocols = DB_PROTOCOLS[dbType]
|
||||
if (!validProtocols.some((p) => protocol.startsWith(p))) {
|
||||
return null
|
||||
}
|
||||
|
||||
const defaults = DB_DEFAULTS[dbType]
|
||||
const host = url.hostname || 'localhost'
|
||||
const port = url.port || '5432'
|
||||
const database = url.pathname.replace(/^\//, '') || 'postgres'
|
||||
const user = url.username || 'postgres'
|
||||
const port = url.port || defaults.port
|
||||
const database = url.pathname.replace(/^\//, '') || defaults.database
|
||||
const user = url.username || defaults.user
|
||||
const password = decodeURIComponent(url.password || '')
|
||||
|
||||
// Check for SSL in query params
|
||||
|
|
@ -53,9 +73,16 @@ function parseConnectionString(connectionString: string): {
|
|||
}
|
||||
}
|
||||
|
||||
export function AddConnectionDialog({ open, onOpenChange }: AddConnectionDialogProps) {
|
||||
export function AddConnectionDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
connection: editConnection
|
||||
}: AddConnectionDialogProps) {
|
||||
const addConnection = useConnectionStore((s) => s.addConnection)
|
||||
const updateConnection = useConnectionStore((s) => s.updateConnection)
|
||||
const isEditMode = !!editConnection
|
||||
|
||||
const [dbType, setDbType] = useState<DatabaseType>('postgresql')
|
||||
const [inputMode, setInputMode] = useState<InputMode>('manual')
|
||||
const [connectionString, setConnectionString] = useState('')
|
||||
const [parseError, setParseError] = useState<string | null>(null)
|
||||
|
|
@ -73,6 +100,40 @@ export function AddConnectionDialog({ open, onOpenChange }: AddConnectionDialogP
|
|||
const [testResult, setTestResult] = useState<'success' | 'error' | null>(null)
|
||||
const [testError, setTestError] = useState<string | null>(null)
|
||||
|
||||
// Populate form when editing
|
||||
useEffect(() => {
|
||||
if (editConnection && open) {
|
||||
setDbType(editConnection.dbType || 'postgresql')
|
||||
setName(editConnection.name)
|
||||
setHost(editConnection.host)
|
||||
setPort(String(editConnection.port))
|
||||
setDatabase(editConnection.database)
|
||||
setUser(editConnection.user)
|
||||
setPassword(editConnection.password || '')
|
||||
setSsl(editConnection.ssl || false)
|
||||
setInputMode('manual')
|
||||
setConnectionString('')
|
||||
setParseError(null)
|
||||
setTestResult(null)
|
||||
setTestError(null)
|
||||
}
|
||||
}, [editConnection, open])
|
||||
|
||||
const handleDbTypeChange = (newType: DatabaseType) => {
|
||||
setDbType(newType)
|
||||
const defaults = DB_DEFAULTS[newType]
|
||||
setPort(defaults.port)
|
||||
setUser(defaults.user)
|
||||
if (defaults.database) {
|
||||
setDatabase(defaults.database)
|
||||
}
|
||||
// Clear connection string when switching types
|
||||
setConnectionString('')
|
||||
setParseError(null)
|
||||
setTestResult(null)
|
||||
setTestError(null)
|
||||
}
|
||||
|
||||
const handleConnectionStringChange = (value: string) => {
|
||||
setConnectionString(value)
|
||||
setParseError(null)
|
||||
|
|
@ -81,7 +142,7 @@ export function AddConnectionDialog({ open, onOpenChange }: AddConnectionDialogP
|
|||
return
|
||||
}
|
||||
|
||||
const parsed = parseConnectionString(value)
|
||||
const parsed = parseConnectionString(value, dbType)
|
||||
if (parsed) {
|
||||
setHost(parsed.host)
|
||||
setPort(parsed.port)
|
||||
|
|
@ -90,11 +151,16 @@ export function AddConnectionDialog({ open, onOpenChange }: AddConnectionDialogP
|
|||
setPassword(parsed.password)
|
||||
setSsl(parsed.ssl)
|
||||
} else {
|
||||
setParseError('Invalid connection string format')
|
||||
const expectedFormat =
|
||||
dbType === 'mysql'
|
||||
? 'mysql://user:password@host:3306/database'
|
||||
: 'postgresql://user:password@host:5432/database'
|
||||
setParseError(`Invalid connection string format. Expected: ${expectedFormat}`)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setDbType('postgresql')
|
||||
setInputMode('manual')
|
||||
setConnectionString('')
|
||||
setParseError(null)
|
||||
|
|
@ -115,14 +181,15 @@ export function AddConnectionDialog({ open, onOpenChange }: AddConnectionDialogP
|
|||
}
|
||||
|
||||
const getConnectionConfig = () => ({
|
||||
id: crypto.randomUUID(),
|
||||
id: editConnection?.id || crypto.randomUUID(),
|
||||
name: name || `${host}/${database}`,
|
||||
host,
|
||||
port: parseInt(port, 10),
|
||||
database,
|
||||
user,
|
||||
password: password || undefined,
|
||||
ssl
|
||||
ssl,
|
||||
dbType
|
||||
})
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
|
|
@ -154,16 +221,22 @@ export function AddConnectionDialog({ open, onOpenChange }: AddConnectionDialogP
|
|||
try {
|
||||
const config = getConnectionConfig()
|
||||
|
||||
// Save to persistent storage
|
||||
const result = await window.api.connections.add(config)
|
||||
|
||||
if (result.success && result.data) {
|
||||
// Add to local store
|
||||
addConnection(result.data)
|
||||
if (isEditMode) {
|
||||
// Update existing connection
|
||||
await updateConnection(config.id, config)
|
||||
handleClose()
|
||||
} else {
|
||||
setTestResult('error')
|
||||
setTestError(result.error || 'Failed to save connection')
|
||||
// Save new connection to persistent storage
|
||||
const result = await window.api.connections.add(config)
|
||||
|
||||
if (result.success && result.data) {
|
||||
// Add to local store
|
||||
addConnection(result.data)
|
||||
handleClose()
|
||||
} else {
|
||||
setTestResult('error')
|
||||
setTestError(result.error || 'Failed to save connection')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setTestResult('error')
|
||||
|
|
@ -184,15 +257,45 @@ export function AddConnectionDialog({ open, onOpenChange }: AddConnectionDialogP
|
|||
<SheetHeader>
|
||||
<SheetTitle className="flex items-center gap-2">
|
||||
<Database className="size-5" />
|
||||
Add Connection
|
||||
{isEditMode ? 'Edit Connection' : 'Add Connection'}
|
||||
</SheetTitle>
|
||||
<SheetDescription>
|
||||
Add a new PostgreSQL database connection. Your credentials are stored securely on your
|
||||
device.
|
||||
{isEditMode
|
||||
? 'Update your database connection settings.'
|
||||
: 'Add a new database connection. Your credentials are stored securely on your device.'}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 py-4 px-4">
|
||||
{/* Database Type Selector */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Database Type</label>
|
||||
<div className="flex rounded-lg border bg-muted p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDbTypeChange('postgresql')}
|
||||
className={`flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
|
||||
dbType === 'postgresql'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
PostgreSQL
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDbTypeChange('mysql')}
|
||||
className={`flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
|
||||
dbType === 'mysql'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
MySQL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Mode Toggle */}
|
||||
<div className="flex rounded-lg border bg-muted p-1">
|
||||
<button
|
||||
|
|
@ -243,13 +346,20 @@ export function AddConnectionDialog({ open, onOpenChange }: AddConnectionDialogP
|
|||
</label>
|
||||
<Input
|
||||
id="connection-string"
|
||||
placeholder="postgresql://user:password@host:5432/database"
|
||||
placeholder={
|
||||
dbType === 'mysql'
|
||||
? 'mysql://user:password@host:3306/database'
|
||||
: 'postgresql://user:password@host:5432/database'
|
||||
}
|
||||
value={connectionString}
|
||||
onChange={(e) => handleConnectionStringChange(e.target.value)}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Format: postgresql://user:password@host:port/database
|
||||
Format:{' '}
|
||||
{dbType === 'mysql'
|
||||
? 'mysql://user:password@host:port/database'
|
||||
: 'postgresql://user:password@host:port/database'}
|
||||
</p>
|
||||
{parseError && <p className="text-xs text-destructive">{parseError}</p>}
|
||||
{connectionString && !parseError && (
|
||||
|
|
@ -389,8 +499,10 @@ export function AddConnectionDialog({ open, onOpenChange }: AddConnectionDialogP
|
|||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Saving...
|
||||
{isEditMode ? 'Updating...' : 'Saving...'}
|
||||
</>
|
||||
) : isEditMode ? (
|
||||
'Update Connection'
|
||||
) : (
|
||||
'Save Connection'
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ChevronDown, Database, Plus, Settings, Loader2 } from 'lucide-react'
|
||||
import { ChevronDown, Plus, Settings, Loader2, Pencil, Trash2 } from 'lucide-react'
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -12,10 +12,76 @@ import {
|
|||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'
|
||||
import { useConnectionStore } from '@/stores'
|
||||
import { useConnectionStore, type Connection } from '@/stores'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { AddConnectionDialog } from './add-connection-dialog'
|
||||
import type { DatabaseType } from '@shared/index'
|
||||
|
||||
// Database type icons as simple SVG components
|
||||
function PostgreSQLIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
<text x="8" y="17" fontSize="8" fontWeight="bold" fill="currentColor" stroke="none">
|
||||
PG
|
||||
</text>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MySQLIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
||||
<path d="M4 6v6c0 1.66 3.58 3 8 3s8-1.34 8-3V6" />
|
||||
<path d="M4 12v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function DatabaseIcon({
|
||||
dbType,
|
||||
className
|
||||
}: {
|
||||
dbType: DatabaseType | undefined
|
||||
className?: string
|
||||
}) {
|
||||
switch (dbType) {
|
||||
case 'mysql':
|
||||
return <MySQLIcon className={className} />
|
||||
case 'postgresql':
|
||||
default:
|
||||
return <PostgreSQLIcon className={className} />
|
||||
}
|
||||
}
|
||||
|
||||
export function ConnectionSwitcher() {
|
||||
const navigate = useNavigate()
|
||||
|
|
@ -24,9 +90,12 @@ export function ConnectionSwitcher() {
|
|||
const setActiveConnection = useConnectionStore((s) => s.setActiveConnection)
|
||||
const setConnectionStatus = useConnectionStore((s) => s.setConnectionStatus)
|
||||
const initializeConnections = useConnectionStore((s) => s.initializeConnections)
|
||||
const removeConnection = useConnectionStore((s) => s.removeConnection)
|
||||
const isInitialized = useConnectionStore((s) => s.isInitialized)
|
||||
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [editingConnection, setEditingConnection] = useState<Connection | null>(null)
|
||||
const [deletingConnection, setDeletingConnection] = useState<Connection | null>(null)
|
||||
|
||||
// Initialize connections from persistent storage on mount
|
||||
useEffect(() => {
|
||||
|
|
@ -50,6 +119,23 @@ export function ConnectionSwitcher() {
|
|||
navigate({ to: '/settings' })
|
||||
}
|
||||
|
||||
const handleEditConnection = (e: React.MouseEvent, connection: Connection) => {
|
||||
e.stopPropagation()
|
||||
setEditingConnection(connection)
|
||||
}
|
||||
|
||||
const handleDeleteConnection = (e: React.MouseEvent, connection: Connection) => {
|
||||
e.stopPropagation()
|
||||
setDeletingConnection(connection)
|
||||
}
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (deletingConnection) {
|
||||
await removeConnection(deletingConnection.id)
|
||||
setDeletingConnection(null)
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state while initializing
|
||||
if (!isInitialized) {
|
||||
return (
|
||||
|
|
@ -91,7 +177,7 @@ export function ConnectionSwitcher() {
|
|||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton className="w-fit px-1.5">
|
||||
<div className="relative flex aspect-square size-5 items-center justify-center">
|
||||
<Database className="size-4 text-sidebar-primary" />
|
||||
<DatabaseIcon dbType={activeConnection?.dbType} className="size-4 text-sidebar-primary" />
|
||||
{activeConnection?.isConnected && (
|
||||
<span className="absolute -bottom-0.5 -right-0.5 size-2 rounded-full bg-green-500 ring-1 ring-sidebar" />
|
||||
)}
|
||||
|
|
@ -118,25 +204,46 @@ export function ConnectionSwitcher() {
|
|||
<DropdownMenuItem
|
||||
key={connection.id}
|
||||
onClick={() => handleSelectConnection(connection.id)}
|
||||
className="gap-2 p-2"
|
||||
className="gap-2 p-2 group"
|
||||
disabled={connection.isConnecting}
|
||||
>
|
||||
<div className="relative flex size-6 items-center justify-center rounded-xs border">
|
||||
{connection.isConnecting ? (
|
||||
<Loader2 className="size-4 shrink-0 animate-spin" />
|
||||
) : (
|
||||
<Database className="size-4 shrink-0" />
|
||||
<DatabaseIcon dbType={connection.dbType} className="size-4 shrink-0" />
|
||||
)}
|
||||
{connection.isConnected && !connection.isConnecting && (
|
||||
<span className="absolute -bottom-0.5 -right-0.5 size-2 rounded-full bg-green-500 ring-1 ring-background" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<span className="font-medium">{connection.name}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium">{connection.name}</span>
|
||||
<span className="text-[10px] px-1 py-0.5 rounded bg-muted text-muted-foreground uppercase">
|
||||
{connection.dbType === 'mysql' ? 'MySQL' : 'PG'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{connection.host}:{connection.port}/{connection.database}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => handleEditConnection(e, connection)}
|
||||
className="p-1 hover:bg-muted rounded"
|
||||
title="Edit connection"
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleDeleteConnection(e, connection)}
|
||||
className="p-1 hover:bg-destructive/10 hover:text-destructive rounded"
|
||||
title="Delete connection"
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{index < 9 && <DropdownMenuShortcut>⌘⇧{index + 1}</DropdownMenuShortcut>}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
|
|
@ -156,7 +263,40 @@ export function ConnectionSwitcher() {
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
<AddConnectionDialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen} />
|
||||
|
||||
{/* Add/Edit Connection Dialog */}
|
||||
<AddConnectionDialog
|
||||
open={isAddDialogOpen || !!editingConnection}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setIsAddDialogOpen(false)
|
||||
setEditingConnection(null)
|
||||
}
|
||||
}}
|
||||
connection={editingConnection}
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={!!deletingConnection} onOpenChange={() => setDeletingConnection(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete connection?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{deletingConnection?.name}"? This action cannot be
|
||||
undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { create } from 'zustand'
|
||||
import { ConnectionConfig, SchemaInfo, TableInfo, ColumnInfo } from '@shared/index'
|
||||
import { ConnectionConfig, SchemaInfo, TableInfo, ColumnInfo, DatabaseType } from '@shared/index'
|
||||
|
||||
export interface Connection {
|
||||
id: string
|
||||
|
|
@ -11,6 +11,7 @@ export interface Connection {
|
|||
password?: string
|
||||
ssl?: boolean
|
||||
group?: string
|
||||
dbType: DatabaseType
|
||||
}
|
||||
|
||||
export interface ConnectionWithStatus extends Connection {
|
||||
|
|
@ -59,6 +60,8 @@ interface ConnectionState {
|
|||
// Helper to convert ConnectionConfig to ConnectionWithStatus
|
||||
const toConnectionWithStatus = (config: ConnectionConfig): ConnectionWithStatus => ({
|
||||
...config,
|
||||
// Default to postgresql for backward compatibility with existing connections
|
||||
dbType: config.dbType || 'postgresql',
|
||||
isConnected: false,
|
||||
isConnecting: false
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export interface ConnectionConfig {
|
|||
user: string;
|
||||
password?: string;
|
||||
ssl?: boolean;
|
||||
dbType: DatabaseType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
109
pnpm-lock.yaml
109
pnpm-lock.yaml
|
|
@ -101,6 +101,9 @@ importers:
|
|||
monaco-editor:
|
||||
specifier: ^0.55.1
|
||||
version: 0.55.1
|
||||
mysql2:
|
||||
specifier: ^3.15.3
|
||||
version: 3.15.3
|
||||
pg:
|
||||
specifier: ^8.16.3
|
||||
version: 8.16.3
|
||||
|
|
@ -197,7 +200,7 @@ importers:
|
|||
version: 2.1.1
|
||||
drizzle-orm:
|
||||
specifier: ^0.44.7
|
||||
version: 0.44.7(@types/pg@8.15.6)(better-sqlite3@12.4.6)(pg@8.16.3)(postgres@3.4.7)
|
||||
version: 0.44.7(@types/pg@8.15.6)(better-sqlite3@12.4.6)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)
|
||||
lucide-react:
|
||||
specifier: ^0.555.0
|
||||
version: 0.555.0(react@19.2.0)
|
||||
|
|
@ -240,7 +243,7 @@ importers:
|
|||
version: 9.39.1(jiti@2.6.1)
|
||||
eslint-config-next:
|
||||
specifier: 16.0.5
|
||||
version: 16.0.5(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
version: 16.0.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
tailwindcss:
|
||||
specifier: ^4
|
||||
version: 4.1.17
|
||||
|
|
@ -2217,6 +2220,10 @@ packages:
|
|||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
aws-ssl-profiles@1.1.2:
|
||||
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
axe-core@4.11.0:
|
||||
resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -2571,6 +2578,10 @@ packages:
|
|||
delegates@1.0.0:
|
||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||
|
||||
denque@2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -3135,6 +3146,9 @@ packages:
|
|||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
deprecated: This package is no longer supported.
|
||||
|
||||
generate-function@2.3.1:
|
||||
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
|
||||
|
||||
generator-function@2.0.1:
|
||||
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -3306,6 +3320,10 @@ packages:
|
|||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
|
|
@ -3432,6 +3450,9 @@ packages:
|
|||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
is-property@1.0.2:
|
||||
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
|
||||
|
||||
is-regex@1.2.1:
|
||||
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -3689,6 +3710,9 @@ packages:
|
|||
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
long@5.3.2:
|
||||
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
|
|
@ -3711,6 +3735,10 @@ packages:
|
|||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
lru.min@1.1.3:
|
||||
resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==}
|
||||
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
|
||||
|
||||
lucide-react@0.555.0:
|
||||
resolution: {integrity: sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==}
|
||||
peerDependencies:
|
||||
|
|
@ -3844,6 +3872,14 @@ packages:
|
|||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
mysql2@3.15.3:
|
||||
resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==}
|
||||
engines: {node: '>= 8.0'}
|
||||
|
||||
named-placeholders@1.1.3:
|
||||
resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
|
|
@ -4363,6 +4399,9 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
seq-queue@0.0.5:
|
||||
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
|
||||
|
||||
serialize-error@7.0.1:
|
||||
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -4478,6 +4517,10 @@ packages:
|
|||
resolution: {integrity: sha512-0bJOPQrRO/JkjQhiThVayq0hOKnI1tHI+2OTkmT7TGtc6kqS+V7kveeMzRW+RNQGxofmTmet9ILvztyuxv0cJQ==}
|
||||
hasBin: true
|
||||
|
||||
sqlstring@2.3.3:
|
||||
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
ssri@9.0.1:
|
||||
resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==}
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
|
|
@ -6872,6 +6915,8 @@ snapshots:
|
|||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
|
||||
aws-ssl-profiles@1.1.2: {}
|
||||
|
||||
axe-core@4.11.0: {}
|
||||
|
||||
axobject-query@4.1.0: {}
|
||||
|
|
@ -7255,6 +7300,8 @@ snapshots:
|
|||
|
||||
delegates@1.0.0: {}
|
||||
|
||||
denque@2.1.0: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
|
@ -7325,10 +7372,11 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
drizzle-orm@0.44.7(@types/pg@8.15.6)(better-sqlite3@12.4.6)(pg@8.16.3)(postgres@3.4.7):
|
||||
drizzle-orm@0.44.7(@types/pg@8.15.6)(better-sqlite3@12.4.6)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7):
|
||||
optionalDependencies:
|
||||
'@types/pg': 8.15.6
|
||||
better-sqlite3: 12.4.6
|
||||
mysql2: 3.15.3
|
||||
pg: 8.16.3
|
||||
postgres: 3.4.7
|
||||
|
||||
|
|
@ -7619,13 +7667,13 @@ snapshots:
|
|||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
eslint-config-next@16.0.5(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
|
||||
eslint-config-next@16.0.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 16.0.5
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@2.6.1))
|
||||
|
|
@ -7651,7 +7699,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
|
|
@ -7662,22 +7710,21 @@ snapshots:
|
|||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
|
||||
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
|
||||
eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
|
|
@ -7688,7 +7735,7 @@ snapshots:
|
|||
doctrine: 2.1.0
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
|
@ -7699,8 +7746,6 @@ snapshots:
|
|||
semver: 6.3.1
|
||||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
|
|
@ -7995,6 +8040,10 @@ snapshots:
|
|||
strip-ansi: 6.0.1
|
||||
wide-align: 1.1.5
|
||||
|
||||
generate-function@2.3.1:
|
||||
dependencies:
|
||||
is-property: 1.0.2
|
||||
|
||||
generator-function@2.0.1: {}
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
|
@ -8198,6 +8247,10 @@ snapshots:
|
|||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
|
@ -8315,6 +8368,8 @@ snapshots:
|
|||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
is-property@1.0.2: {}
|
||||
|
||||
is-regex@1.2.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
|
|
@ -8534,6 +8589,8 @@ snapshots:
|
|||
chalk: 4.1.2
|
||||
is-unicode-supported: 0.1.0
|
||||
|
||||
long@5.3.2: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
|
@ -8552,6 +8609,8 @@ snapshots:
|
|||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
lru.min@1.1.3: {}
|
||||
|
||||
lucide-react@0.555.0(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
|
@ -8682,6 +8741,22 @@ snapshots:
|
|||
|
||||
ms@2.1.3: {}
|
||||
|
||||
mysql2@3.15.3:
|
||||
dependencies:
|
||||
aws-ssl-profiles: 1.1.2
|
||||
denque: 2.1.0
|
||||
generate-function: 2.3.1
|
||||
iconv-lite: 0.7.0
|
||||
long: 5.3.2
|
||||
lru.min: 1.1.3
|
||||
named-placeholders: 1.1.3
|
||||
seq-queue: 0.0.5
|
||||
sqlstring: 2.3.3
|
||||
|
||||
named-placeholders@1.1.3:
|
||||
dependencies:
|
||||
lru-cache: 7.18.3
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
napi-build-utils@2.0.0: {}
|
||||
|
|
@ -9238,6 +9313,8 @@ snapshots:
|
|||
|
||||
semver@7.7.3: {}
|
||||
|
||||
seq-queue@0.0.5: {}
|
||||
|
||||
serialize-error@7.0.1:
|
||||
dependencies:
|
||||
type-fest: 0.13.1
|
||||
|
|
@ -9398,6 +9475,8 @@ snapshots:
|
|||
argparse: 2.0.1
|
||||
nearley: 2.20.1
|
||||
|
||||
sqlstring@2.3.3: {}
|
||||
|
||||
ssri@9.0.1:
|
||||
dependencies:
|
||||
minipass: 3.3.6
|
||||
|
|
|
|||
460
seeds/acme_saas_mysql_seed.sql
Normal file
460
seeds/acme_saas_mysql_seed.sql
Normal file
|
|
@ -0,0 +1,460 @@
|
|||
-- ============================================================================
|
||||
-- ACME SaaS - Sample Database for data-peek (MySQL Version)
|
||||
-- ============================================================================
|
||||
-- A fictional SaaS platform database with realistic structure and data.
|
||||
-- Perfect for testing, demos, and screenshots.
|
||||
-- ============================================================================
|
||||
|
||||
-- Use the database
|
||||
-- CREATE DATABASE IF NOT EXISTS playground;
|
||||
-- USE playground;
|
||||
|
||||
-- ============================================================================
|
||||
-- TABLES
|
||||
-- ============================================================================
|
||||
|
||||
-- Users
|
||||
CREATE TABLE users (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
avatar_url VARCHAR(512),
|
||||
email_verified_at DATETIME,
|
||||
last_login_at DATETIME,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_users_email (email),
|
||||
INDEX idx_users_created_at (created_at)
|
||||
);
|
||||
|
||||
-- Organizations
|
||||
CREATE TABLE organizations (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(100) NOT NULL UNIQUE,
|
||||
plan ENUM('free', 'starter', 'pro', 'enterprise') NOT NULL DEFAULT 'free',
|
||||
logo_url VARCHAR(512),
|
||||
website VARCHAR(255),
|
||||
billing_email VARCHAR(255),
|
||||
metadata JSON,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_organizations_slug (slug),
|
||||
INDEX idx_organizations_plan (plan)
|
||||
);
|
||||
|
||||
-- Memberships (users <-> organizations)
|
||||
CREATE TABLE memberships (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
organization_id CHAR(36) NOT NULL,
|
||||
role ENUM('owner', 'admin', 'member', 'viewer') NOT NULL DEFAULT 'member',
|
||||
invited_by CHAR(36),
|
||||
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE KEY unique_user_org (user_id, organization_id),
|
||||
INDEX idx_memberships_user_id (user_id),
|
||||
INDEX idx_memberships_organization_id (organization_id),
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (invited_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Projects
|
||||
CREATE TABLE projects (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
organization_id CHAR(36) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
status ENUM('active', 'paused', 'archived', 'deleted') NOT NULL DEFAULT 'active',
|
||||
settings JSON,
|
||||
created_by CHAR(36),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_projects_organization_id (organization_id),
|
||||
INDEX idx_projects_status (status),
|
||||
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- API Keys
|
||||
CREATE TABLE api_keys (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
organization_id CHAR(36) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
key_prefix VARCHAR(12) NOT NULL,
|
||||
key_hash VARCHAR(255) NOT NULL,
|
||||
scopes JSON,
|
||||
last_used_at DATETIME,
|
||||
expires_at DATETIME,
|
||||
created_by CHAR(36),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
revoked_at DATETIME,
|
||||
|
||||
INDEX idx_api_keys_organization_id (organization_id),
|
||||
INDEX idx_api_keys_key_prefix (key_prefix),
|
||||
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Subscriptions
|
||||
CREATE TABLE subscriptions (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
organization_id CHAR(36) NOT NULL,
|
||||
plan ENUM('free', 'starter', 'pro', 'enterprise') NOT NULL,
|
||||
status ENUM('active', 'past_due', 'canceled', 'trialing') NOT NULL DEFAULT 'active',
|
||||
stripe_subscription_id VARCHAR(100),
|
||||
stripe_customer_id VARCHAR(100),
|
||||
current_period_start DATETIME NOT NULL,
|
||||
current_period_end DATETIME NOT NULL,
|
||||
cancel_at DATETIME,
|
||||
canceled_at DATETIME,
|
||||
trial_end DATETIME,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_subscriptions_organization_id (organization_id),
|
||||
INDEX idx_subscriptions_status (status),
|
||||
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Invoices
|
||||
CREATE TABLE invoices (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
organization_id CHAR(36) NOT NULL,
|
||||
subscription_id CHAR(36),
|
||||
stripe_invoice_id VARCHAR(100),
|
||||
amount_cents INT NOT NULL,
|
||||
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
|
||||
status ENUM('draft', 'pending', 'paid', 'failed', 'refunded') NOT NULL DEFAULT 'pending',
|
||||
description VARCHAR(500),
|
||||
invoice_url VARCHAR(512),
|
||||
pdf_url VARCHAR(512),
|
||||
due_date DATETIME,
|
||||
paid_at DATETIME,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_invoices_organization_id (organization_id),
|
||||
INDEX idx_invoices_status (status),
|
||||
INDEX idx_invoices_created_at (created_at),
|
||||
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Events (audit log / activity feed)
|
||||
CREATE TABLE events (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
organization_id CHAR(36),
|
||||
user_id CHAR(36),
|
||||
type ENUM('user.created', 'user.updated', 'org.created', 'project.created', 'subscription.started', 'subscription.canceled', 'invoice.paid', 'api_key.created') NOT NULL,
|
||||
resource_type VARCHAR(50),
|
||||
resource_id CHAR(36),
|
||||
payload JSON,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent VARCHAR(500),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_events_organization_id (organization_id),
|
||||
INDEX idx_events_user_id (user_id),
|
||||
INDEX idx_events_type (type),
|
||||
INDEX idx_events_created_at (created_at),
|
||||
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Feature Flags
|
||||
CREATE TABLE feature_flags (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
rollout_percentage TINYINT UNSIGNED DEFAULT 0,
|
||||
allowed_organizations JSON,
|
||||
metadata JSON,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_feature_flags_name (name),
|
||||
INDEX idx_feature_flags_enabled (enabled),
|
||||
|
||||
CONSTRAINT chk_rollout CHECK (rollout_percentage >= 0 AND rollout_percentage <= 100)
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- SEED DATA
|
||||
-- ============================================================================
|
||||
|
||||
-- Users (20 users with realistic data)
|
||||
INSERT INTO users (id, email, name, avatar_url, email_verified_at, last_login_at, created_at) VALUES
|
||||
('a1b2c3d4-e5f6-4789-abcd-111111111111', 'sarah.chen@gmail.com', 'Sarah Chen', 'https://api.dicebear.com/7.x/avataaars/svg?seed=sarah', DATE_SUB(NOW(), INTERVAL 89 DAY), DATE_SUB(NOW(), INTERVAL 2 HOUR), DATE_SUB(NOW(), INTERVAL 90 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-222222222222', 'marcus.johnson@outlook.com', 'Marcus Johnson', 'https://api.dicebear.com/7.x/avataaars/svg?seed=marcus', DATE_SUB(NOW(), INTERVAL 84 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 85 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-333333333333', 'priya.patel@company.io', 'Priya Patel', 'https://api.dicebear.com/7.x/avataaars/svg?seed=priya', DATE_SUB(NOW(), INTERVAL 79 DAY), DATE_SUB(NOW(), INTERVAL 5 HOUR), DATE_SUB(NOW(), INTERVAL 80 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-444444444444', 'alex.rivera@techstartup.com', 'Alex Rivera', NULL, DATE_SUB(NOW(), INTERVAL 59 DAY), DATE_SUB(NOW(), INTERVAL 3 DAY), DATE_SUB(NOW(), INTERVAL 60 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-555555555555', 'emma.wilson@agency.co', 'Emma Wilson', 'https://api.dicebear.com/7.x/avataaars/svg?seed=emma', DATE_SUB(NOW(), INTERVAL 44 DAY), DATE_SUB(NOW(), INTERVAL 12 HOUR), DATE_SUB(NOW(), INTERVAL 45 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-666666666666', 'james.kim@devhouse.io', 'James Kim', 'https://api.dicebear.com/7.x/avataaars/svg?seed=james', DATE_SUB(NOW(), INTERVAL 39 DAY), DATE_SUB(NOW(), INTERVAL 6 HOUR), DATE_SUB(NOW(), INTERVAL 40 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-777777777777', 'olivia.martinez@freelance.dev', 'Olivia Martinez', NULL, NULL, DATE_SUB(NOW(), INTERVAL 7 DAY), DATE_SUB(NOW(), INTERVAL 35 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-888888888888', 'david.thompson@bigcorp.com', 'David Thompson', 'https://api.dicebear.com/7.x/avataaars/svg?seed=david', DATE_SUB(NOW(), INTERVAL 29 DAY), DATE_SUB(NOW(), INTERVAL 4 HOUR), DATE_SUB(NOW(), INTERVAL 30 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-999999999999', 'sofia.andersson@nordic.tech', 'Sofia Andersson', 'https://api.dicebear.com/7.x/avataaars/svg?seed=sofia', DATE_SUB(NOW(), INTERVAL 24 DAY), DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 25 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-aaaaaaaaaaaa', 'ryan.ogrady@startup.ie', 'Ryan O\'Grady', 'https://api.dicebear.com/7.x/avataaars/svg?seed=ryan', DATE_SUB(NOW(), INTERVAL 19 DAY), DATE_SUB(NOW(), INTERVAL 8 HOUR), DATE_SUB(NOW(), INTERVAL 20 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-bbbbbbbbbbbb', 'mei.zhang@enterprise.cn', 'Mei Zhang', 'https://api.dicebear.com/7.x/avataaars/svg?seed=mei', DATE_SUB(NOW(), INTERVAL 14 DAY), DATE_SUB(NOW(), INTERVAL 1 HOUR), DATE_SUB(NOW(), INTERVAL 15 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-cccccccccccc', 'lucas.fernandez@latam.io', 'Lucas Fernandez', NULL, DATE_SUB(NOW(), INTERVAL 9 DAY), DATE_SUB(NOW(), INTERVAL 16 HOUR), DATE_SUB(NOW(), INTERVAL 10 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-dddddddddddd', 'anna.kowalski@polish.dev', 'Anna Kowalski', 'https://api.dicebear.com/7.x/avataaars/svg?seed=anna', DATE_SUB(NOW(), INTERVAL 7 DAY), DATE_SUB(NOW(), INTERVAL 3 HOUR), DATE_SUB(NOW(), INTERVAL 8 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-eeeeeeeeeeee', 'tom.nguyen@saigon.tech', 'Tom Nguyen', 'https://api.dicebear.com/7.x/avataaars/svg?seed=tom', DATE_SUB(NOW(), INTERVAL 6 DAY), DATE_SUB(NOW(), INTERVAL 45 MINUTE), DATE_SUB(NOW(), INTERVAL 7 DAY)),
|
||||
('a1b2c3d4-e5f6-4789-abcd-ffffffffffff', 'lisa.jackson@remote.work', 'Lisa Jackson', 'https://api.dicebear.com/7.x/avataaars/svg?seed=lisa', DATE_SUB(NOW(), INTERVAL 4 DAY), DATE_SUB(NOW(), INTERVAL 20 HOUR), DATE_SUB(NOW(), INTERVAL 5 DAY)),
|
||||
('b1b2c3d4-e5f6-4789-abcd-111111111111', 'ahmed.hassan@cairo.dev', 'Ahmed Hassan', NULL, NULL, NULL, DATE_SUB(NOW(), INTERVAL 4 DAY)),
|
||||
('b1b2c3d4-e5f6-4789-abcd-222222222222', 'nina.volkov@moscow.io', 'Nina Volkov', 'https://api.dicebear.com/7.x/avataaars/svg?seed=nina', DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 5 HOUR), DATE_SUB(NOW(), INTERVAL 3 DAY)),
|
||||
('b1b2c3d4-e5f6-4789-abcd-333333333333', 'chris.baker@london.agency', 'Chris Baker', 'https://api.dicebear.com/7.x/avataaars/svg?seed=chris', DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 30 MINUTE), DATE_SUB(NOW(), INTERVAL 2 DAY)),
|
||||
('b1b2c3d4-e5f6-4789-abcd-444444444444', 'yuki.tanaka@tokyo.tech', 'Yuki Tanaka', 'https://api.dicebear.com/7.x/avataaars/svg?seed=yuki', DATE_SUB(NOW(), INTERVAL 12 HOUR), DATE_SUB(NOW(), INTERVAL 2 HOUR), DATE_SUB(NOW(), INTERVAL 1 DAY)),
|
||||
('b1b2c3d4-e5f6-4789-abcd-555555555555', 'fatima.ali@dubai.startup', 'Fatima Ali', NULL, NULL, NULL, DATE_SUB(NOW(), INTERVAL 6 HOUR));
|
||||
|
||||
-- Organizations (8 organizations with varied plans)
|
||||
INSERT INTO organizations (id, name, slug, plan, logo_url, website, billing_email, metadata, created_at) VALUES
|
||||
('c1c2c3c4-e5f6-4789-abcd-111111111111', 'Acme Corporation', 'acme-corp', 'enterprise', 'https://api.dicebear.com/7.x/identicon/svg?seed=acme', 'https://acme.example.com', 'billing@acme.example.com', '{"industry": "technology", "size": "500-1000", "region": "north-america"}', DATE_SUB(NOW(), INTERVAL 88 DAY)),
|
||||
('c1c2c3c4-e5f6-4789-abcd-222222222222', 'Startup Labs', 'startup-labs', 'pro', 'https://api.dicebear.com/7.x/identicon/svg?seed=startuplabs', 'https://startuplabs.io', 'finance@startuplabs.io', '{"industry": "saas", "size": "10-50", "region": "europe"}', DATE_SUB(NOW(), INTERVAL 75 DAY)),
|
||||
('c1c2c3c4-e5f6-4789-abcd-333333333333', 'DevHouse Agency', 'devhouse', 'pro', 'https://api.dicebear.com/7.x/identicon/svg?seed=devhouse', 'https://devhouse.io', NULL, '{"industry": "agency", "size": "50-100"}', DATE_SUB(NOW(), INTERVAL 60 DAY)),
|
||||
('c1c2c3c4-e5f6-4789-abcd-444444444444', 'Nordic Tech Solutions', 'nordic-tech', 'starter', NULL, 'https://nordic-tech.se', 'accounts@nordic-tech.se', '{"industry": "consulting", "size": "10-50", "region": "europe"}', DATE_SUB(NOW(), INTERVAL 45 DAY)),
|
||||
('c1c2c3c4-e5f6-4789-abcd-555555555555', 'Solo Developer', 'solo-dev', 'free', NULL, NULL, NULL, '{}', DATE_SUB(NOW(), INTERVAL 30 DAY)),
|
||||
('c1c2c3c4-e5f6-4789-abcd-666666666666', 'Enterprise Global Inc', 'enterprise-global', 'enterprise', 'https://api.dicebear.com/7.x/identicon/svg?seed=enterprise', 'https://enterprise-global.com', 'ap@enterprise-global.com', '{"industry": "finance", "size": "1000+", "region": "global"}', DATE_SUB(NOW(), INTERVAL 20 DAY)),
|
||||
('c1c2c3c4-e5f6-4789-abcd-777777777777', 'Fresh Startup', 'fresh-startup', 'starter', NULL, NULL, 'hello@freshstartup.co', '{"industry": "fintech", "size": "1-10"}', DATE_SUB(NOW(), INTERVAL 10 DAY)),
|
||||
('c1c2c3c4-e5f6-4789-abcd-888888888888', 'Trial Company', 'trial-company', 'free', NULL, NULL, NULL, '{}', DATE_SUB(NOW(), INTERVAL 3 DAY));
|
||||
|
||||
-- Memberships
|
||||
INSERT INTO memberships (id, user_id, organization_id, role, invited_by, joined_at) VALUES
|
||||
-- Acme Corporation (enterprise) - 6 members
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-111111111111', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'owner', NULL, DATE_SUB(NOW(), INTERVAL 88 DAY)),
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-222222222222', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'admin', 'a1b2c3d4-e5f6-4789-abcd-111111111111', DATE_SUB(NOW(), INTERVAL 85 DAY)),
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-333333333333', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'member', 'a1b2c3d4-e5f6-4789-abcd-111111111111', DATE_SUB(NOW(), INTERVAL 80 DAY)),
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-888888888888', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'member', 'a1b2c3d4-e5f6-4789-abcd-222222222222', DATE_SUB(NOW(), INTERVAL 30 DAY)),
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-bbbbbbbbbbbb', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'viewer', 'a1b2c3d4-e5f6-4789-abcd-222222222222', DATE_SUB(NOW(), INTERVAL 15 DAY)),
|
||||
(UUID(), 'b1b2c3d4-e5f6-4789-abcd-333333333333', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'member', 'a1b2c3d4-e5f6-4789-abcd-111111111111', DATE_SUB(NOW(), INTERVAL 2 DAY)),
|
||||
|
||||
-- Startup Labs (pro) - 4 members
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-444444444444', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'owner', NULL, DATE_SUB(NOW(), INTERVAL 75 DAY)),
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-555555555555', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'admin', 'a1b2c3d4-e5f6-4789-abcd-444444444444', DATE_SUB(NOW(), INTERVAL 45 DAY)),
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-cccccccccccc', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'member', 'a1b2c3d4-e5f6-4789-abcd-444444444444', DATE_SUB(NOW(), INTERVAL 10 DAY)),
|
||||
(UUID(), 'b1b2c3d4-e5f6-4789-abcd-444444444444', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'member', 'a1b2c3d4-e5f6-4789-abcd-555555555555', DATE_SUB(NOW(), INTERVAL 1 DAY)),
|
||||
|
||||
-- DevHouse Agency (pro) - 3 members
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-666666666666', 'c1c2c3c4-e5f6-4789-abcd-333333333333', 'owner', NULL, DATE_SUB(NOW(), INTERVAL 60 DAY)),
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-777777777777', 'c1c2c3c4-e5f6-4789-abcd-333333333333', 'admin', 'a1b2c3d4-e5f6-4789-abcd-666666666666', DATE_SUB(NOW(), INTERVAL 35 DAY)),
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-dddddddddddd', 'c1c2c3c4-e5f6-4789-abcd-333333333333', 'member', 'a1b2c3d4-e5f6-4789-abcd-666666666666', DATE_SUB(NOW(), INTERVAL 8 DAY)),
|
||||
|
||||
-- Nordic Tech Solutions (starter) - 2 members
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-999999999999', 'c1c2c3c4-e5f6-4789-abcd-444444444444', 'owner', NULL, DATE_SUB(NOW(), INTERVAL 45 DAY)),
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-aaaaaaaaaaaa', 'c1c2c3c4-e5f6-4789-abcd-444444444444', 'member', 'a1b2c3d4-e5f6-4789-abcd-999999999999', DATE_SUB(NOW(), INTERVAL 20 DAY)),
|
||||
|
||||
-- Solo Developer (free) - 1 member
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-eeeeeeeeeeee', 'c1c2c3c4-e5f6-4789-abcd-555555555555', 'owner', NULL, DATE_SUB(NOW(), INTERVAL 30 DAY)),
|
||||
|
||||
-- Enterprise Global (enterprise) - 4 members
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-ffffffffffff', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'owner', NULL, DATE_SUB(NOW(), INTERVAL 20 DAY)),
|
||||
(UUID(), 'b1b2c3d4-e5f6-4789-abcd-111111111111', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'admin', 'a1b2c3d4-e5f6-4789-abcd-ffffffffffff', DATE_SUB(NOW(), INTERVAL 4 DAY)),
|
||||
(UUID(), 'b1b2c3d4-e5f6-4789-abcd-222222222222', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'member', 'a1b2c3d4-e5f6-4789-abcd-ffffffffffff', DATE_SUB(NOW(), INTERVAL 3 DAY)),
|
||||
(UUID(), 'b1b2c3d4-e5f6-4789-abcd-555555555555', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'viewer', 'b1b2c3d4-e5f6-4789-abcd-111111111111', DATE_SUB(NOW(), INTERVAL 6 HOUR)),
|
||||
|
||||
-- Fresh Startup (starter) - 2 members
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-cccccccccccc', 'c1c2c3c4-e5f6-4789-abcd-777777777777', 'owner', NULL, DATE_SUB(NOW(), INTERVAL 10 DAY)),
|
||||
(UUID(), 'a1b2c3d4-e5f6-4789-abcd-dddddddddddd', 'c1c2c3c4-e5f6-4789-abcd-777777777777', 'admin', 'a1b2c3d4-e5f6-4789-abcd-cccccccccccc', DATE_SUB(NOW(), INTERVAL 5 DAY)),
|
||||
|
||||
-- Trial Company (free) - 1 member
|
||||
(UUID(), 'b1b2c3d4-e5f6-4789-abcd-444444444444', 'c1c2c3c4-e5f6-4789-abcd-888888888888', 'owner', NULL, DATE_SUB(NOW(), INTERVAL 3 DAY));
|
||||
|
||||
-- Projects (15 projects across organizations)
|
||||
INSERT INTO projects (id, organization_id, name, description, status, settings, created_by, created_at) VALUES
|
||||
-- Acme Corporation projects
|
||||
('d1d2d3d4-e5f6-4789-abcd-111111111111', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'Customer Portal', 'Main customer-facing web application', 'active', '{"environment": "production", "framework": "next.js"}', 'a1b2c3d4-e5f6-4789-abcd-111111111111', DATE_SUB(NOW(), INTERVAL 87 DAY)),
|
||||
('d1d2d3d4-e5f6-4789-abcd-222222222222', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'Internal Dashboard', 'Admin tools and analytics', 'active', '{"environment": "production", "framework": "react"}', 'a1b2c3d4-e5f6-4789-abcd-222222222222', DATE_SUB(NOW(), INTERVAL 80 DAY)),
|
||||
('d1d2d3d4-e5f6-4789-abcd-333333333333', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'Mobile App v2', 'React Native mobile application', 'active', '{"environment": "staging", "framework": "react-native"}', 'a1b2c3d4-e5f6-4789-abcd-333333333333', DATE_SUB(NOW(), INTERVAL 45 DAY)),
|
||||
('d1d2d3d4-e5f6-4789-abcd-444444444444', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'Legacy API', 'Deprecated REST API (sunset Q2 2024)', 'paused', '{"environment": "production", "deprecated": true}', 'a1b2c3d4-e5f6-4789-abcd-111111111111', DATE_SUB(NOW(), INTERVAL 300 DAY)),
|
||||
('d1d2d3d4-e5f6-4789-abcd-555555555555', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'Data Pipeline', NULL, 'archived', '{}', 'a1b2c3d4-e5f6-4789-abcd-222222222222', DATE_SUB(NOW(), INTERVAL 200 DAY)),
|
||||
|
||||
-- Startup Labs projects
|
||||
('d1d2d3d4-e5f6-4789-abcd-666666666666', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'SaaS Platform', 'Core product platform', 'active', '{"environment": "production"}', 'a1b2c3d4-e5f6-4789-abcd-444444444444', DATE_SUB(NOW(), INTERVAL 74 DAY)),
|
||||
('d1d2d3d4-e5f6-4789-abcd-777777777777', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'Marketing Site', 'Public website and blog', 'active', '{"environment": "production", "cms": "contentful"}', 'a1b2c3d4-e5f6-4789-abcd-555555555555', DATE_SUB(NOW(), INTERVAL 50 DAY)),
|
||||
('d1d2d3d4-e5f6-4789-abcd-888888888888', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'Analytics Service', 'Internal metrics and tracking', 'active', '{}', 'a1b2c3d4-e5f6-4789-abcd-444444444444', DATE_SUB(NOW(), INTERVAL 30 DAY)),
|
||||
|
||||
-- DevHouse Agency projects
|
||||
('d1d2d3d4-e5f6-4789-abcd-999999999999', 'c1c2c3c4-e5f6-4789-abcd-333333333333', 'Client: TechCorp', 'E-commerce platform build', 'active', '{"client": "techcorp", "deadline": "2024-06-30"}', 'a1b2c3d4-e5f6-4789-abcd-666666666666', DATE_SUB(NOW(), INTERVAL 55 DAY)),
|
||||
('d1d2d3d4-e5f6-4789-abcd-aaaaaaaaaaaa', 'c1c2c3c4-e5f6-4789-abcd-333333333333', 'Client: HealthApp', 'Healthcare mobile app', 'active', '{"client": "healthapp"}', 'a1b2c3d4-e5f6-4789-abcd-777777777777', DATE_SUB(NOW(), INTERVAL 20 DAY)),
|
||||
('d1d2d3d4-e5f6-4789-abcd-bbbbbbbbbbbb', 'c1c2c3c4-e5f6-4789-abcd-333333333333', 'Client: OldBank', 'Legacy system maintenance', 'archived', '{"client": "oldbank", "archived_reason": "contract_ended"}', 'a1b2c3d4-e5f6-4789-abcd-666666666666', DATE_SUB(NOW(), INTERVAL 180 DAY)),
|
||||
|
||||
-- Nordic Tech Solutions
|
||||
('d1d2d3d4-e5f6-4789-abcd-cccccccccccc', 'c1c2c3c4-e5f6-4789-abcd-444444444444', 'Consulting Tools', 'Internal tooling suite', 'active', '{}', 'a1b2c3d4-e5f6-4789-abcd-999999999999', DATE_SUB(NOW(), INTERVAL 40 DAY)),
|
||||
|
||||
-- Solo Developer
|
||||
('d1d2d3d4-e5f6-4789-abcd-dddddddddddd', 'c1c2c3c4-e5f6-4789-abcd-555555555555', 'Side Project', 'Personal SaaS experiment', 'active', '{"personal": true}', 'a1b2c3d4-e5f6-4789-abcd-eeeeeeeeeeee', DATE_SUB(NOW(), INTERVAL 28 DAY)),
|
||||
|
||||
-- Enterprise Global
|
||||
('d1d2d3d4-e5f6-4789-abcd-eeeeeeeeeeee', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'Trading Platform', 'Real-time trading system', 'active', '{"environment": "production", "compliance": "sox"}', 'a1b2c3d4-e5f6-4789-abcd-ffffffffffff', DATE_SUB(NOW(), INTERVAL 18 DAY)),
|
||||
('d1d2d3d4-e5f6-4789-abcd-ffffffffffff', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'Risk Analytics', 'Risk assessment dashboard', 'active', '{"environment": "staging"}', 'b1b2c3d4-e5f6-4789-abcd-111111111111', DATE_SUB(NOW(), INTERVAL 3 DAY));
|
||||
|
||||
-- API Keys
|
||||
INSERT INTO api_keys (id, organization_id, name, key_prefix, key_hash, scopes, last_used_at, expires_at, created_by, created_at, revoked_at) VALUES
|
||||
-- Acme Corporation
|
||||
('e1e2e3e4-e5f6-4789-abcd-111111111111', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'Production API', 'pk_live_acm', 'sha256$a1b2c3d4e5f6g7h8i9j0...', '["read", "write"]', DATE_SUB(NOW(), INTERVAL 5 MINUTE), DATE_ADD(NOW(), INTERVAL 1 YEAR), 'a1b2c3d4-e5f6-4789-abcd-111111111111', DATE_SUB(NOW(), INTERVAL 85 DAY), NULL),
|
||||
('e1e2e3e4-e5f6-4789-abcd-222222222222', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'Staging API', 'pk_test_acm', 'sha256$b2c3d4e5f6g7h8i9j0k1...', '["read", "write", "delete"]', DATE_SUB(NOW(), INTERVAL 2 HOUR), NULL, 'a1b2c3d4-e5f6-4789-abcd-222222222222', DATE_SUB(NOW(), INTERVAL 80 DAY), NULL),
|
||||
('e1e2e3e4-e5f6-4789-abcd-333333333333', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'CI/CD Pipeline', 'pk_ci_acme', 'sha256$c3d4e5f6g7h8i9j0k1l2...', '["read"]', DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_ADD(NOW(), INTERVAL 90 DAY), 'a1b2c3d4-e5f6-4789-abcd-333333333333', DATE_SUB(NOW(), INTERVAL 60 DAY), NULL),
|
||||
('e1e2e3e4-e5f6-4789-abcd-444444444444', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'Old Integration', 'pk_old_acme', 'sha256$d4e5f6g7h8i9j0k1l2m3...', '["read"]', DATE_SUB(NOW(), INTERVAL 180 DAY), DATE_SUB(NOW(), INTERVAL 30 DAY), 'a1b2c3d4-e5f6-4789-abcd-111111111111', DATE_SUB(NOW(), INTERVAL 365 DAY), DATE_SUB(NOW(), INTERVAL 30 DAY)),
|
||||
|
||||
-- Startup Labs
|
||||
('e1e2e3e4-e5f6-4789-abcd-555555555555', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'Main API Key', 'pk_live_stl', 'sha256$e5f6g7h8i9j0k1l2m3n4...', '["read", "write"]', DATE_SUB(NOW(), INTERVAL 30 MINUTE), NULL, 'a1b2c3d4-e5f6-4789-abcd-444444444444', DATE_SUB(NOW(), INTERVAL 70 DAY), NULL),
|
||||
('e1e2e3e4-e5f6-4789-abcd-666666666666', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'Webhook Service', 'pk_whk_stl', 'sha256$f6g7h8i9j0k1l2m3n4o5...', '["read"]', DATE_SUB(NOW(), INTERVAL 4 HOUR), NULL, 'a1b2c3d4-e5f6-4789-abcd-555555555555', DATE_SUB(NOW(), INTERVAL 45 DAY), NULL),
|
||||
|
||||
-- DevHouse Agency
|
||||
('e1e2e3e4-e5f6-4789-abcd-777777777777', 'c1c2c3c4-e5f6-4789-abcd-333333333333', 'Client Projects', 'pk_live_dvh', 'sha256$g7h8i9j0k1l2m3n4o5p6...', '["read", "write", "delete"]', DATE_SUB(NOW(), INTERVAL 12 HOUR), NULL, 'a1b2c3d4-e5f6-4789-abcd-666666666666', DATE_SUB(NOW(), INTERVAL 55 DAY), NULL),
|
||||
|
||||
-- Enterprise Global
|
||||
('e1e2e3e4-e5f6-4789-abcd-888888888888', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'Trading API', 'pk_live_ent', 'sha256$h8i9j0k1l2m3n4o5p6q7...', '["read", "write", "trade"]', DATE_SUB(NOW(), INTERVAL 2 MINUTE), NULL, 'a1b2c3d4-e5f6-4789-abcd-ffffffffffff', DATE_SUB(NOW(), INTERVAL 15 DAY), NULL),
|
||||
('e1e2e3e4-e5f6-4789-abcd-999999999999', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'Read-Only Analytics', 'pk_ro_ent', 'sha256$i9j0k1l2m3n4o5p6q7r8...', '["read"]', NULL, NULL, 'b1b2c3d4-e5f6-4789-abcd-111111111111', DATE_SUB(NOW(), INTERVAL 3 DAY), NULL);
|
||||
|
||||
-- Subscriptions
|
||||
INSERT INTO subscriptions (id, organization_id, plan, status, stripe_subscription_id, stripe_customer_id, current_period_start, current_period_end, cancel_at, canceled_at, trial_end, created_at) VALUES
|
||||
-- Acme Corporation - Enterprise (active)
|
||||
('f1f2f3f4-e5f6-4789-abcd-111111111111', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'enterprise', 'active', 'sub_1NxYz123456789', 'cus_AcmeCorp001', DATE_SUB(NOW(), INTERVAL 28 DAY), DATE_ADD(NOW(), INTERVAL 2 DAY), NULL, NULL, NULL, DATE_SUB(NOW(), INTERVAL 88 DAY)),
|
||||
|
||||
-- Startup Labs - Pro (active)
|
||||
('f1f2f3f4-e5f6-4789-abcd-222222222222', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'pro', 'active', 'sub_2AbCd234567890', 'cus_StartupLabs01', DATE_SUB(NOW(), INTERVAL 15 DAY), DATE_ADD(NOW(), INTERVAL 15 DAY), NULL, NULL, NULL, DATE_SUB(NOW(), INTERVAL 75 DAY)),
|
||||
|
||||
-- DevHouse Agency - Pro (past_due)
|
||||
('f1f2f3f4-e5f6-4789-abcd-333333333333', 'c1c2c3c4-e5f6-4789-abcd-333333333333', 'pro', 'past_due', 'sub_3CdEf345678901', 'cus_DevHouse0001', DATE_SUB(NOW(), INTERVAL 35 DAY), DATE_SUB(NOW(), INTERVAL 5 DAY), NULL, NULL, NULL, DATE_SUB(NOW(), INTERVAL 60 DAY)),
|
||||
|
||||
-- Nordic Tech Solutions - Starter (active)
|
||||
('f1f2f3f4-e5f6-4789-abcd-444444444444', 'c1c2c3c4-e5f6-4789-abcd-444444444444', 'starter', 'active', 'sub_4EfGh456789012', 'cus_NordicTech01', DATE_SUB(NOW(), INTERVAL 10 DAY), DATE_ADD(NOW(), INTERVAL 20 DAY), NULL, NULL, NULL, DATE_SUB(NOW(), INTERVAL 45 DAY)),
|
||||
|
||||
-- Enterprise Global - Enterprise (active)
|
||||
('f1f2f3f4-e5f6-4789-abcd-555555555555', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'enterprise', 'active', 'sub_5GhIj567890123', 'cus_EntGlobal001', DATE_SUB(NOW(), INTERVAL 5 DAY), DATE_ADD(NOW(), INTERVAL 25 DAY), NULL, NULL, NULL, DATE_SUB(NOW(), INTERVAL 20 DAY)),
|
||||
|
||||
-- Fresh Startup - Starter (trialing)
|
||||
('f1f2f3f4-e5f6-4789-abcd-666666666666', 'c1c2c3c4-e5f6-4789-abcd-777777777777', 'starter', 'trialing', 'sub_6IjKl678901234', 'cus_FreshStart01', DATE_SUB(NOW(), INTERVAL 10 DAY), DATE_ADD(NOW(), INTERVAL 20 DAY), NULL, NULL, DATE_ADD(NOW(), INTERVAL 4 DAY), DATE_SUB(NOW(), INTERVAL 10 DAY)),
|
||||
|
||||
-- Trial Company - Canceled subscription (for history)
|
||||
('f1f2f3f4-e5f6-4789-abcd-777777777777', 'c1c2c3c4-e5f6-4789-abcd-888888888888', 'starter', 'canceled', 'sub_7KlMn789012345', 'cus_TrialCo00001', DATE_SUB(NOW(), INTERVAL 30 DAY), DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 5 DAY), NULL, DATE_SUB(NOW(), INTERVAL 30 DAY));
|
||||
|
||||
-- Invoices
|
||||
INSERT INTO invoices (id, organization_id, subscription_id, stripe_invoice_id, amount_cents, currency, status, description, invoice_url, due_date, paid_at, created_at) VALUES
|
||||
-- Acme Corporation invoices
|
||||
('11111111-e5f6-4789-abcd-111111111111', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'f1f2f3f4-e5f6-4789-abcd-111111111111', 'in_acme_001', 99900, 'USD', 'paid', 'Enterprise Plan - Monthly', 'https://invoice.stripe.com/i/acme_001', DATE_SUB(NOW(), INTERVAL 58 DAY), DATE_SUB(NOW(), INTERVAL 57 DAY), DATE_SUB(NOW(), INTERVAL 60 DAY)),
|
||||
('11111111-e5f6-4789-abcd-222222222222', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'f1f2f3f4-e5f6-4789-abcd-111111111111', 'in_acme_002', 99900, 'USD', 'paid', 'Enterprise Plan - Monthly', 'https://invoice.stripe.com/i/acme_002', DATE_SUB(NOW(), INTERVAL 28 DAY), DATE_SUB(NOW(), INTERVAL 27 DAY), DATE_SUB(NOW(), INTERVAL 30 DAY)),
|
||||
('11111111-e5f6-4789-abcd-333333333333', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'f1f2f3f4-e5f6-4789-abcd-111111111111', 'in_acme_003', 99900, 'USD', 'pending', 'Enterprise Plan - Monthly', 'https://invoice.stripe.com/i/acme_003', DATE_ADD(NOW(), INTERVAL 2 DAY), NULL, NOW()),
|
||||
|
||||
-- Startup Labs invoices
|
||||
('11111111-e5f6-4789-abcd-444444444444', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'f1f2f3f4-e5f6-4789-abcd-222222222222', 'in_stl_001', 4900, 'USD', 'paid', 'Pro Plan - Monthly', 'https://invoice.stripe.com/i/stl_001', DATE_SUB(NOW(), INTERVAL 45 DAY), DATE_SUB(NOW(), INTERVAL 45 DAY), DATE_SUB(NOW(), INTERVAL 45 DAY)),
|
||||
('11111111-e5f6-4789-abcd-555555555555', 'c1c2c3c4-e5f6-4789-abcd-222222222222', 'f1f2f3f4-e5f6-4789-abcd-222222222222', 'in_stl_002', 4900, 'USD', 'paid', 'Pro Plan - Monthly', 'https://invoice.stripe.com/i/stl_002', DATE_SUB(NOW(), INTERVAL 15 DAY), DATE_SUB(NOW(), INTERVAL 14 DAY), DATE_SUB(NOW(), INTERVAL 15 DAY)),
|
||||
|
||||
-- DevHouse Agency invoices (one failed)
|
||||
('11111111-e5f6-4789-abcd-666666666666', 'c1c2c3c4-e5f6-4789-abcd-333333333333', 'f1f2f3f4-e5f6-4789-abcd-333333333333', 'in_dvh_001', 4900, 'USD', 'paid', 'Pro Plan - Monthly', 'https://invoice.stripe.com/i/dvh_001', DATE_SUB(NOW(), INTERVAL 35 DAY), DATE_SUB(NOW(), INTERVAL 34 DAY), DATE_SUB(NOW(), INTERVAL 35 DAY)),
|
||||
('11111111-e5f6-4789-abcd-777777777777', 'c1c2c3c4-e5f6-4789-abcd-333333333333', 'f1f2f3f4-e5f6-4789-abcd-333333333333', 'in_dvh_002', 4900, 'USD', 'failed', 'Pro Plan - Monthly', 'https://invoice.stripe.com/i/dvh_002', DATE_SUB(NOW(), INTERVAL 5 DAY), NULL, DATE_SUB(NOW(), INTERVAL 5 DAY)),
|
||||
|
||||
-- Nordic Tech Solutions
|
||||
('11111111-e5f6-4789-abcd-888888888888', 'c1c2c3c4-e5f6-4789-abcd-444444444444', 'f1f2f3f4-e5f6-4789-abcd-444444444444', 'in_nts_001', 1900, 'USD', 'paid', 'Starter Plan - Monthly', 'https://invoice.stripe.com/i/nts_001', DATE_SUB(NOW(), INTERVAL 10 DAY), DATE_SUB(NOW(), INTERVAL 10 DAY), DATE_SUB(NOW(), INTERVAL 10 DAY)),
|
||||
|
||||
-- Enterprise Global
|
||||
('11111111-e5f6-4789-abcd-999999999999', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'f1f2f3f4-e5f6-4789-abcd-555555555555', 'in_ent_001', 99900, 'USD', 'paid', 'Enterprise Plan - Monthly', 'https://invoice.stripe.com/i/ent_001', DATE_SUB(NOW(), INTERVAL 5 DAY), DATE_SUB(NOW(), INTERVAL 4 DAY), DATE_SUB(NOW(), INTERVAL 5 DAY)),
|
||||
|
||||
-- Refunded invoice example
|
||||
('11111111-e5f6-4789-abcd-aaaaaaaaaaaa', 'c1c2c3c4-e5f6-4789-abcd-888888888888', 'f1f2f3f4-e5f6-4789-abcd-777777777777', 'in_trial_001', 1900, 'USD', 'refunded', 'Starter Plan - Monthly (Refunded)', 'https://invoice.stripe.com/i/trial_001', DATE_SUB(NOW(), INTERVAL 30 DAY), DATE_SUB(NOW(), INTERVAL 29 DAY), DATE_SUB(NOW(), INTERVAL 30 DAY));
|
||||
|
||||
-- Events (audit log)
|
||||
INSERT INTO events (id, organization_id, user_id, type, resource_type, resource_id, payload, ip_address, user_agent, created_at) VALUES
|
||||
-- User signups
|
||||
('22222222-e5f6-4789-abcd-111111111111', NULL, 'a1b2c3d4-e5f6-4789-abcd-111111111111', 'user.created', 'user', 'a1b2c3d4-e5f6-4789-abcd-111111111111', '{"email": "sarah.chen@gmail.com"}', '73.162.214.130', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', DATE_SUB(NOW(), INTERVAL 90 DAY)),
|
||||
('22222222-e5f6-4789-abcd-222222222222', NULL, 'b1b2c3d4-e5f6-4789-abcd-444444444444', 'user.created', 'user', 'b1b2c3d4-e5f6-4789-abcd-444444444444', '{"email": "yuki.tanaka@tokyo.tech"}', '103.5.140.219', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', DATE_SUB(NOW(), INTERVAL 1 DAY)),
|
||||
('22222222-e5f6-4789-abcd-333333333333', NULL, 'b1b2c3d4-e5f6-4789-abcd-555555555555', 'user.created', 'user', 'b1b2c3d4-e5f6-4789-abcd-555555555555', '{"email": "fatima.ali@dubai.startup"}', '94.56.229.180', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)', DATE_SUB(NOW(), INTERVAL 6 HOUR)),
|
||||
|
||||
-- Organization creation
|
||||
('22222222-e5f6-4789-abcd-444444444444', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'a1b2c3d4-e5f6-4789-abcd-111111111111', 'org.created', 'organization', 'c1c2c3c4-e5f6-4789-abcd-111111111111', '{"name": "Acme Corporation", "plan": "free"}', '73.162.214.130', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', DATE_SUB(NOW(), INTERVAL 88 DAY)),
|
||||
('22222222-e5f6-4789-abcd-555555555555', 'c1c2c3c4-e5f6-4789-abcd-888888888888', 'b1b2c3d4-e5f6-4789-abcd-444444444444', 'org.created', 'organization', 'c1c2c3c4-e5f6-4789-abcd-888888888888', '{"name": "Trial Company", "plan": "free"}', '103.5.140.219', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', DATE_SUB(NOW(), INTERVAL 3 DAY)),
|
||||
|
||||
-- Project creation
|
||||
('22222222-e5f6-4789-abcd-666666666666', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'a1b2c3d4-e5f6-4789-abcd-111111111111', 'project.created', 'project', 'd1d2d3d4-e5f6-4789-abcd-111111111111', '{"name": "Customer Portal"}', '73.162.214.130', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', DATE_SUB(NOW(), INTERVAL 87 DAY)),
|
||||
('22222222-e5f6-4789-abcd-777777777777', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'b1b2c3d4-e5f6-4789-abcd-111111111111', 'project.created', 'project', 'd1d2d3d4-e5f6-4789-abcd-ffffffffffff', '{"name": "Risk Analytics"}', '185.45.12.89', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', DATE_SUB(NOW(), INTERVAL 3 DAY)),
|
||||
|
||||
-- Subscription events
|
||||
('22222222-e5f6-4789-abcd-888888888888', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'a1b2c3d4-e5f6-4789-abcd-111111111111', 'subscription.started', 'subscription', 'f1f2f3f4-e5f6-4789-abcd-111111111111', '{"plan": "enterprise", "amount": 999}', '73.162.214.130', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', DATE_SUB(NOW(), INTERVAL 88 DAY)),
|
||||
('22222222-e5f6-4789-abcd-999999999999', 'c1c2c3c4-e5f6-4789-abcd-888888888888', 'b1b2c3d4-e5f6-4789-abcd-444444444444', 'subscription.canceled', 'subscription', 'f1f2f3f4-e5f6-4789-abcd-777777777777', '{"reason": "too_expensive", "feedback": "Will come back when we have more budget"}', '103.5.140.219', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', DATE_SUB(NOW(), INTERVAL 5 DAY)),
|
||||
|
||||
-- Invoice paid
|
||||
('22222222-e5f6-4789-abcd-aaaaaaaaaaaa', 'c1c2c3c4-e5f6-4789-abcd-111111111111', NULL, 'invoice.paid', 'invoice', '11111111-e5f6-4789-abcd-222222222222', '{"amount": 999, "currency": "USD"}', NULL, NULL, DATE_SUB(NOW(), INTERVAL 27 DAY)),
|
||||
('22222222-e5f6-4789-abcd-bbbbbbbbbbbb', 'c1c2c3c4-e5f6-4789-abcd-666666666666', NULL, 'invoice.paid', 'invoice', '11111111-e5f6-4789-abcd-999999999999', '{"amount": 999, "currency": "USD"}', NULL, NULL, DATE_SUB(NOW(), INTERVAL 4 DAY)),
|
||||
|
||||
-- API key created
|
||||
('22222222-e5f6-4789-abcd-cccccccccccc', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'a1b2c3d4-e5f6-4789-abcd-111111111111', 'api_key.created', 'api_key', 'e1e2e3e4-e5f6-4789-abcd-111111111111', '{"name": "Production API", "scopes": ["read", "write"]}', '73.162.214.130', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', DATE_SUB(NOW(), INTERVAL 85 DAY)),
|
||||
('22222222-e5f6-4789-abcd-dddddddddddd', 'c1c2c3c4-e5f6-4789-abcd-666666666666', 'a1b2c3d4-e5f6-4789-abcd-ffffffffffff', 'api_key.created', 'api_key', 'e1e2e3e4-e5f6-4789-abcd-888888888888', '{"name": "Trading API", "scopes": ["read", "write", "trade"]}', '185.45.12.89', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', DATE_SUB(NOW(), INTERVAL 15 DAY)),
|
||||
|
||||
-- Recent activity burst
|
||||
('22222222-e5f6-4789-abcd-eeeeeeeeeeee', 'c1c2c3c4-e5f6-4789-abcd-111111111111', 'b1b2c3d4-e5f6-4789-abcd-333333333333', 'user.updated', 'user', 'b1b2c3d4-e5f6-4789-abcd-333333333333', '{"changes": ["avatar_url"]}', '88.120.45.67', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', DATE_SUB(NOW(), INTERVAL 30 MINUTE));
|
||||
|
||||
-- Feature Flags
|
||||
INSERT INTO feature_flags (id, name, description, enabled, rollout_percentage, allowed_organizations, metadata, created_at) VALUES
|
||||
('33333333-e5f6-4789-abcd-111111111111', 'new_dashboard', 'Redesigned analytics dashboard with charts', TRUE, 100, '[]', '{"version": "2.0", "designer": "emma"}', DATE_SUB(NOW(), INTERVAL 60 DAY)),
|
||||
('33333333-e5f6-4789-abcd-222222222222', 'ai_suggestions', 'AI-powered query suggestions in editor', TRUE, 25, '["c1c2c3c4-e5f6-4789-abcd-111111111111", "c1c2c3c4-e5f6-4789-abcd-666666666666"]', '{"model": "gpt-4", "beta": true}', DATE_SUB(NOW(), INTERVAL 30 DAY)),
|
||||
('33333333-e5f6-4789-abcd-333333333333', 'dark_mode_v2', 'Updated dark mode color scheme', TRUE, 50, '[]', '{}', DATE_SUB(NOW(), INTERVAL 20 DAY)),
|
||||
('33333333-e5f6-4789-abcd-444444444444', 'export_pdf', 'Export reports to PDF format', FALSE, 0, '[]', '{"status": "in_development", "eta": "2024-Q2"}', DATE_SUB(NOW(), INTERVAL 15 DAY)),
|
||||
('33333333-e5f6-4789-abcd-555555555555', 'team_collaboration', 'Real-time collaboration features', TRUE, 10, '["c1c2c3c4-e5f6-4789-abcd-111111111111"]', '{"websocket": true}', DATE_SUB(NOW(), INTERVAL 10 DAY)),
|
||||
('33333333-e5f6-4789-abcd-666666666666', 'sso_okta', 'Okta SSO integration', FALSE, 0, '["c1c2c3c4-e5f6-4789-abcd-666666666666"]', '{"enterprise_only": true}', DATE_SUB(NOW(), INTERVAL 5 DAY)),
|
||||
('33333333-e5f6-4789-abcd-777777777777', 'billing_v2', 'New billing system with usage-based pricing', FALSE, 0, '[]', '{"migration_required": true}', DATE_SUB(NOW(), INTERVAL 2 DAY));
|
||||
|
||||
-- ============================================================================
|
||||
-- HELPER VIEWS
|
||||
-- ============================================================================
|
||||
|
||||
-- Organization summary view
|
||||
CREATE VIEW organization_summary AS
|
||||
SELECT
|
||||
o.id,
|
||||
o.name,
|
||||
o.slug,
|
||||
o.plan,
|
||||
COUNT(DISTINCT m.user_id) as member_count,
|
||||
COUNT(DISTINCT CASE WHEN p.status = 'active' THEN p.id END) as project_count,
|
||||
s.status as subscription_status,
|
||||
o.created_at
|
||||
FROM organizations o
|
||||
LEFT JOIN memberships m ON o.id = m.organization_id
|
||||
LEFT JOIN projects p ON o.id = p.organization_id
|
||||
LEFT JOIN subscriptions s ON o.id = s.organization_id
|
||||
GROUP BY o.id, o.name, o.slug, o.plan, s.status, o.created_at;
|
||||
|
||||
-- Recent activity view
|
||||
CREATE VIEW recent_activity AS
|
||||
SELECT
|
||||
e.id,
|
||||
e.type,
|
||||
e.created_at,
|
||||
u.name as user_name,
|
||||
u.email as user_email,
|
||||
o.name as organization_name,
|
||||
e.payload
|
||||
FROM events e
|
||||
LEFT JOIN users u ON e.user_id = u.id
|
||||
LEFT JOIN organizations o ON e.organization_id = o.id
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT 100;
|
||||
|
||||
-- ============================================================================
|
||||
-- DONE!
|
||||
-- ============================================================================
|
||||
--
|
||||
-- Summary:
|
||||
-- - 20 users
|
||||
-- - 8 organizations (across all plan tiers)
|
||||
-- - 23 memberships
|
||||
-- - 15 projects
|
||||
-- - 9 API keys
|
||||
-- - 7 subscriptions
|
||||
-- - 10 invoices
|
||||
-- - 15 events
|
||||
-- - 7 feature flags
|
||||
-- - 2 helper views
|
||||
--
|
||||
-- MySQL Version - Enjoy! 🚀
|
||||
-- ============================================================================
|
||||
Loading…
Reference in a new issue