Skip to content

SQL Playground — Go source

Run real SQL on sample datasets - or your own schema - right in the browser. Write queries, see formatted results instantly, and export or share them. Powered by sql.js (SQLite WASM); 100% client-side.

This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// sql-playground — polyglot showcase port (Go)
//
// Pure helper logic for the CosmoDev "SQL Playground" tool: splitting a SQL
// script into statements, classifying read-only ones, serializing result sets
// to CSV / Markdown / JSON, and extracting a schema summary from CREATE TABLE
// scripts.
//
// Ported from src/lib/sql-playground.ts (the canonical TypeScript lib that
// powers the live tool). Functionally equivalent: same inputs -> same outputs.
//
// Display source — part of CosmoDev's polyglot tool pages.
package sqlplayground

import (
	"encoding/json"
	"regexp"
	"strconv"
	"strings"
)

// SchemaTable is the { table, columns } summary extracted from a CREATE TABLE
// statement.
type SchemaTable struct {
	Table   string   `json:"table"`
	Columns []string `json:"columns"`
}

// SplitStatements splits a SQL script into top-level statements on ';',
// respecting single-quote string literals. A doubled '' inside a string is an
// escaped literal quote (SQL standard) and does not end the string.
// Whitespace-only statements are dropped.
func SplitStatements(sql string) []string {
	var stmts []string
	var current strings.Builder
	inString := false
	runes := []rune(sql)

	for i := 0; i < len(runes); i++ {
		ch := runes[i]

		switch {
		case inString:
			current.WriteRune(ch)
			if ch == '\'' {
				if i+1 < len(runes) && runes[i+1] == '\'' {
					// Escaped literal quote: consume both runes.
					current.WriteRune(runes[i+1])
					i++
				} else {
					inString = false
				}
			}
		case ch == '\'':
			inString = true
			current.WriteRune(ch)
		case ch == ';':
			trimmed := strings.TrimSpace(current.String())
			if trimmed != "" {
				stmts = append(stmts, trimmed)
			}
			current.Reset()
		default:
			current.WriteRune(ch)
		}
	}

	// Flush any trailing statement that wasn't terminated by ';'.
	trimmed := strings.TrimSpace(current.String())
	if trimmed != "" {
		stmts = append(stmts, trimmed)
	}

	// Guarantee a non-nil slice for parity with empty arrays elsewhere.
	if stmts == nil {
		stmts = []string{}
	}
	return stmts
}

var readOnlyRe = regexp.MustCompile(`(?i)^(SELECT|WITH|VALUES|EXPLAIN|PRAGMA)\b`)

// IsReadOnlyStatement reports whether stmt begins with a read-only keyword
// (SELECT/WITH/VALUES/EXPLAIN/PRAGMA). Such statements are safe to run against
// a snapshot without mutating state.
func IsReadOnlyStatement(stmt string) bool {
	return readOnlyRe.MatchString(strings.TrimSpace(stmt))
}

// FormatScalar renders one cell value as the textual form used in tables/CSV:
// nil -> "NULL", bool/number -> native string, string -> verbatim, anything
// else -> JSON.
func FormatScalar(v any) string {
	if v == nil {
		return "NULL"
	}
	switch val := v.(type) {
	case bool:
		if val {
			return "true"
		}
		return "false"
	case float64:
		// JSON-decoded numbers arrive as float64. Render without trailing
		// fractional noise when the value is a whole number, mirroring JS
		// Number -> String formatting.
		if val == float64(int64(val)) {
			return jsonInt(int64(val))
		}
		return jsonFloat(val)
	case int:
		return jsonInt(int64(val))
	case int64:
		return jsonInt(val)
	case float32:
		return jsonFloat(float64(val))
	case string:
		return val
	default:
		b, err := json.Marshal(val)
		if err != nil {
			return "NULL"
		}
		return string(b)
	}
}

// jsonInt reproduces JavaScript's String(number) for integers (no grouping,
// no exponent). strconv.FormatInt(base 10) gives exactly that.
func jsonInt(i int64) string { return strconv.FormatInt(i, 10) }

// jsonFloat reproduces JavaScript's shortest round-trip float formatting.
// 'g' with -1 precision picks the shortest form that round-trips.
func jsonFloat(f float64) string { return strconv.FormatFloat(f, 'g', -1, 64) }

var needsCsvQuoting = regexp.MustCompile(`[,"\n\r]`)

// csvField formats one cell per RFC-4180-ish CSV: quote if it contains a
// comma, double quote, or newline; double any embedded double quotes.
func csvField(v any) string {
	s := FormatScalar(v)
	if needsCsvQuoting.MatchString(s) {
		return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
	}
	return s
}

// RowsToCSV serializes a result set to CSV (header + one line per record,
// trailing LF).
func RowsToCSV(columns []string, rows [][]any) string {
	var lines []string
	colLine := make([]string, len(columns))
	for i, c := range columns {
		colLine[i] = csvField(c)
	}
	lines = append(lines, strings.Join(colLine, ","))

	for _, row := range rows {
		parts := make([]string, len(row))
		for i, cell := range row {
			parts[i] = csvField(cell)
		}
		lines = append(lines, strings.Join(parts, ","))
	}
	return strings.Join(lines, "\n") + "\n"
}

// RowsToMarkdown serializes a result set as a GitHub-flavored Markdown table.
// Pipes inside cells are escaped with a backslash so they don't break layout.
func RowsToMarkdown(columns []string, rows [][]any) string {
	esc := func(s string) string { return strings.ReplaceAll(s, "|", `\|`) }

	colCells := make([]string, len(columns))
	for i, c := range columns {
		colCells[i] = esc(c)
	}
	header := "| " + strings.Join(colCells, " | ") + " |"
	sepCells := make([]string, len(columns))
	for i := range columns {
		sepCells[i] = "---"
	}
	sep := "| " + strings.Join(sepCells, " | ") + " |"

	lines := []string{header, sep}
	for _, row := range rows {
		cells := make([]string, len(row))
		for i, cell := range row {
			cells[i] = esc(FormatScalar(cell))
		}
		lines = append(lines, "| "+strings.Join(cells, " | ")+" |")
	}
	return strings.Join(lines, "\n") + "\n"
}

// RowsToJSON serializes a result set as a pretty-printed JSON array of objects
// keyed by column name. Missing cells (row shorter than columns) become null.
func RowsToJSON(columns []string, rows [][]any) string {
	out := make([]map[string]any, 0, len(rows))
	for _, row := range rows {
		obj := make(map[string]any, len(columns))
		for i, col := range columns {
			if i < len(row) {
				obj[col] = row[i]
			} else {
				obj[col] = nil
			}
		}
		out = append(out, obj)
	}
	b, _ := json.MarshalIndent(out, "", "  ")
	return string(b)
}

var constraintRe = regexp.MustCompile(`(?i)^(PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK|CONSTRAINT)\b`)
var columnNameRe = regexp.MustCompile("^[`\"']?(\\w+)[`\"']?")

// extractColumnName pulls the column name (lower-cased) from a single column
// definition. Returns "" for table-level constraint lines (PRIMARY KEY,
// FOREIGN KEY, UNIQUE, CHECK, CONSTRAINT), which are not columns.
func extractColumnName(def string) string {
	if def == "" {
		return ""
	}
	if constraintRe.MatchString(def) {
		return ""
	}
	m := columnNameRe.FindStringSubmatch(def)
	if m == nil {
		return ""
	}
	return strings.ToLower(m[1])
}

var headerRe = regexp.MustCompile(`(?i)CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[` + "`" + `\"']?(\w+)[` + "`" + `\"']?\s*\(`)

// SummarizeSchema best-effort extracts { table, columns } summaries from a
// CREATE TABLE script. Handles IF NOT EXISTS, quoted identifiers, and
// parenthesised type/constraint bodies (depth-tracked so a comma inside
// NUMERIC(10,2) doesn't split a column). Returns an empty slice on any error.
func SummarizeSchema(createSql string) []SchemaTable {
	// Defer a recover so a malformed input can never panic the caller.
	defer func() {
		_ = recover()
	}()

	results := []SchemaTable{}
	idxs := headerRe.FindAllStringSubmatchIndex(createSql, -1)
	if idxs == nil {
		return results
	}

	for _, loc := range idxs {
		// loc[0]=match start, loc[1]=match end, loc[2:4]=group1 (table name) span.
		table := strings.ToLower(createSql[loc[2]:loc[3]])
		startIdx := loc[1]

		// Walk forward to find the matching close paren of the table body.
		depth := 1
		i := startIdx
		for i < len(createSql) && depth > 0 {
			switch createSql[i] {
			case '(':
				depth++
			case ')':
				depth--
			}
			i++
		}
		if depth != 0 {
			// Unbalanced parens -> malformed; skip.
			continue
		}

		body := createSql[startIdx : i-1]
		var columns []string

		// Split body on top-level commas only.
		colDepth := 0
		current := strings.Builder{}
		for _, ch := range body {
			switch ch {
			case '(':
				colDepth++
				current.WriteRune(ch)
			case ')':
				colDepth--
				current.WriteRune(ch)
			case ',':
				if colDepth == 0 {
					if col := extractColumnName(strings.TrimSpace(current.String())); col != "" {
						columns = append(columns, col)
					}
					current.Reset()
				} else {
					current.WriteRune(ch)
				}
			default:
				current.WriteRune(ch)
			}
		}
		if col := extractColumnName(strings.TrimSpace(current.String())); col != "" {
			columns = append(columns, col)
		}

		if columns == nil {
			columns = []string{}
		}
		results = append(results, SchemaTable{Table: table, Columns: columns})
	}

	return results
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →