Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions graph/database.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
extend type Database {
structure: DatabaseStructure!
}

type DatabaseStructure {
tables: [DatabaseTable!]!
}

type DatabaseTable {
name: String!
columns: [String!]!
}
31 changes: 31 additions & 0 deletions graph/database.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 8 additions & 6 deletions graph/ent.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion graph/event.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions graph/model/models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion graph/question.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,15 @@ extend type Question {
referenceAnswerResult: SQLExecutionResult! @scope(scope: "question:read")

"""
List of your submissions for this question.
List of your submissions for this question, ordered by submitted at descending.
"""
userSubmissions: [Submission!]!

"""
Get the last submission for this question.
"""
lastSubmission: Submission

"""
Have you tried to solve the question?
"""
Expand Down
26 changes: 24 additions & 2 deletions graph/question.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion graph/user.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions internal/sqlrunner/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,14 @@ type ErrorResponse struct {
func (e ErrorResponse) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}

// DatabaseStructure is the database structure of a schema.
type DatabaseStructure struct {
Tables []DatabaseTable `json:"tables"`
}

// DatabaseTable is the table structure of a schema.
type DatabaseTable struct {
Name string `json:"name"`
Columns []string `json:"columns"`
}
44 changes: 44 additions & 0 deletions internal/sqlrunner/sqlrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,50 @@ func (s *SqlRunner) Query(ctx context.Context, schema, query string) (DataRespon
return respBody.Data, nil
}

func (s *SqlRunner) GetDatabaseStructure(ctx context.Context, schema string) (DatabaseStructure, error) {
// Query SQLite's master table to get all table names
tablesQuery := "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
tablesResp, err := s.Query(ctx, schema, tablesQuery)
if err != nil {
return DatabaseStructure{}, fmt.Errorf("failed to query tables: %w", err)
}

var tables []DatabaseTable

// For each table, get its column information
for _, row := range tablesResp.Rows {
if len(row) == 0 {
continue
}
tableName := row[0]

// Use PRAGMA table_info to get column information
columnsQuery := fmt.Sprintf("PRAGMA table_info(%s)", tableName)
columnsResp, err := s.Query(ctx, schema, columnsQuery)
if err != nil {
return DatabaseStructure{}, fmt.Errorf("query columns for table %s: %w", tableName, err)
}

var columns []string
// PRAGMA table_info returns: cid, name, type, notnull, dflt_value, pk
// We only need the name (index 1)
for _, columnRow := range columnsResp.Rows {
if len(columnRow) > 1 {
columns = append(columns, columnRow[1])
}
}

tables = append(tables, DatabaseTable{
Name: tableName,
Columns: columns,
})
}

return DatabaseStructure{
Tables: tables,
}, nil
}

func (s *SqlRunner) IsHealthy(ctx context.Context) bool {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/healthz", s.cfg.URI), nil)
if err != nil {
Expand Down
149 changes: 149 additions & 0 deletions internal/sqlrunner/sqlrunner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,152 @@ func TestQuery_SchemaError(t *testing.T) {
t.Errorf("Expected SCHEMA_ERROR, got %v", errResp.Code)
}
}

func TestGetDatabaseStructure_Success(t *testing.T) {
s := testhelper.NewSQLRunnerClient(t)

// Create a schema with multiple tables and columns
schema := `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
);
CREATE TABLE posts (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
user_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE categories (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
`

structure, err := s.GetDatabaseStructure(context.Background(), schema)
if err != nil {
t.Fatalf("Expected success, got error: %v", err)
}

// Verify we have the expected number of tables
if len(structure.Tables) != 3 {
t.Errorf("Expected 3 tables, got %d", len(structure.Tables))
}

// Helper function to find a table by name
findTable := func(name string) *sqlrunner.DatabaseTable {
for _, table := range structure.Tables {
if table.Name == name {
return &table
}
}
return nil
}

// Verify users table
usersTable := findTable("users")
if usersTable == nil {
t.Error("Expected to find 'users' table")
} else {
expectedColumns := []string{"id", "name", "email"}
if len(usersTable.Columns) != len(expectedColumns) {
t.Errorf("Expected %d columns in users table, got %d", len(expectedColumns), len(usersTable.Columns))
}
for i, expected := range expectedColumns {
if i >= len(usersTable.Columns) || usersTable.Columns[i] != expected {
t.Errorf("Expected column %d to be '%s', got '%s'", i, expected, usersTable.Columns[i])
}
}
}

// Verify posts table
postsTable := findTable("posts")
if postsTable == nil {
t.Error("Expected to find 'posts' table")
} else {
expectedColumns := []string{"id", "title", "content", "user_id", "created_at"}
if len(postsTable.Columns) != len(expectedColumns) {
t.Errorf("Expected %d columns in posts table, got %d", len(expectedColumns), len(postsTable.Columns))
}
}

// Verify categories table
categoriesTable := findTable("categories")
if categoriesTable == nil {
t.Error("Expected to find 'categories' table")
} else {
expectedColumns := []string{"id", "name"}
if len(categoriesTable.Columns) != len(expectedColumns) {
t.Errorf("Expected %d columns in categories table, got %d", len(expectedColumns), len(categoriesTable.Columns))
}
}
}

func TestGetDatabaseStructure_EmptyDatabase(t *testing.T) {
s := testhelper.NewSQLRunnerClient(t)

// Schema that doesn't create any tables - just a comment
schema := "-- Empty database with no tables"

structure, err := s.GetDatabaseStructure(context.Background(), schema)
if err != nil {
t.Fatalf("Expected success, got error: %v", err)
}

// Should have no tables
if len(structure.Tables) != 0 {
t.Errorf("Expected 0 tables in empty database, got %d", len(structure.Tables))
}
}

func TestGetDatabaseStructure_ErrorHandling(t *testing.T) {
s := testhelper.NewSQLRunnerClient(t)

// Create a schema with syntax error that should fail
schema := "CREATE TABLE invalid_syntax (id int"

_, err := s.GetDatabaseStructure(context.Background(), schema)
if err == nil {
t.Error("Expected error for invalid schema, got nil")
}

// Should be a schema error since the schema creation fails
var errResp *sqlrunner.ErrorResponse
if !errors.As(err, &errResp) {
t.Errorf("Expected ErrorResponse, got %v", err)
}
if errResp.Code != sqlrunner.ErrorCodeSchemaError {
t.Errorf("Expected SCHEMA_ERROR, got %v", errResp.Code)
}
}

func TestGetDatabaseStructure_WithViews(t *testing.T) {
s := testhelper.NewSQLRunnerClient(t)

// Create schema with both tables and views
schema := `
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price DECIMAL(10,2)
);
CREATE VIEW expensive_products AS
SELECT * FROM products WHERE price > 100;
`

structure, err := s.GetDatabaseStructure(context.Background(), schema)
if err != nil {
t.Fatalf("Expected success, got error: %v", err)
}

// Should only include tables, not views (since we filter by type='table')
if len(structure.Tables) != 1 {
t.Errorf("Expected 1 table (views should be excluded), got %d", len(structure.Tables))
}

if structure.Tables[0].Name != "products" {
t.Errorf("Expected table name 'products', got '%s'", structure.Tables[0].Name)
}
}