Upload files to "internal/repl"
This commit is contained in:
1 parent
f098896469
commit
d695d05fbd
3 files changed
+752
No files matched your search
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/repl/completer.go
|
||||
// Назначение: Автодополнение команд, имён коллекций, полей и операторов.
|
||||
// Поддерживает как собственный API, так и интеграцию с chzyer/readline.
|
||||
|
||||
package repl
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
)
|
||||
|
||||
// Completer предоставляет автодополнение.
|
||||
type Completer struct {
|
||||
repl *Repl
|
||||
}
|
||||
|
||||
// NewCompleter создаёт автодополнение.
|
||||
func NewCompleter(r *Repl) *Completer {
|
||||
return &Completer{repl: r}
|
||||
}
|
||||
|
||||
// Complete возвращает список подсказок для заданного префикса.
|
||||
func (c *Completer) Complete(prefix string) []string {
|
||||
if c == nil || c.repl == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Fields(prefix)
|
||||
trailingSpace := strings.HasSuffix(prefix, " ")
|
||||
|
||||
if len(parts) == 0 || (len(parts) == 1 && !trailingSpace) {
|
||||
return c.completeCommand(prefix)
|
||||
}
|
||||
|
||||
cmdName := ""
|
||||
for name := range c.repl.commands {
|
||||
if strings.HasPrefix(prefix, name) {
|
||||
if len(name) > len(cmdName) {
|
||||
cmdName = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cmdName == "" {
|
||||
if len(parts) > 0 {
|
||||
cmdName = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
argsPrefix := strings.TrimPrefix(prefix, cmdName)
|
||||
argsPrefix = strings.TrimSpace(argsPrefix)
|
||||
|
||||
argParts := strings.Fields(argsPrefix)
|
||||
lastArg := ""
|
||||
if len(argParts) > 0 && !trailingSpace {
|
||||
lastArg = argParts[len(argParts)-1]
|
||||
}
|
||||
|
||||
return c.completeArgs(cmdName, lastArg)
|
||||
}
|
||||
|
||||
func (c *Completer) completeCommand(prefix string) []string {
|
||||
var out []string
|
||||
seen := map[string]struct{}{}
|
||||
for name := range c.repl.commands {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
if _, ok := seen[name]; !ok {
|
||||
seen[name] = struct{}{}
|
||||
out = append(out, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extra := []string{
|
||||
"help", "clear", "quit", "exit", "pager",
|
||||
}
|
||||
for _, e := range extra {
|
||||
if strings.HasPrefix(e, prefix) {
|
||||
if _, ok := seen[e]; !ok {
|
||||
seen[e] = struct{}{}
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *Completer) completeArgs(cmdName, lastArg string) []string {
|
||||
switch cmdName {
|
||||
case "use", "drop database", "create slice", "export", "import":
|
||||
return c.completeDatabases(lastArg)
|
||||
case "create collection", "drop collection", "show collections",
|
||||
"show indexes", "show triggers", "show deleted", "count",
|
||||
"stats timestamps", "compress collection":
|
||||
return c.completeCollections(lastArg)
|
||||
case "insert", "find", "update", "delete", "permanent delete", "restore",
|
||||
"findbyindex", "findbytime", "create index", "drop index",
|
||||
"add required", "add unique", "add min", "add max", "add enum",
|
||||
"create trigger", "drop trigger", "enable trigger", "disable trigger",
|
||||
"show timestamps", "doc compression":
|
||||
return c.completeCollectionThenField(cmdName, lastArg)
|
||||
case "migration":
|
||||
return c.completeMigration(lastArg)
|
||||
default:
|
||||
return c.completeOperators(lastArg)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Completer) completeDatabases(prefix string) []string {
|
||||
if c.repl.store == nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, db := range c.repl.store.ListDatabases() {
|
||||
if strings.HasPrefix(db, prefix) {
|
||||
out = append(out, db)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *Completer) completeCollections(prefix string) []string {
|
||||
if c.repl.store == nil || c.repl.currentDB == "" {
|
||||
return nil
|
||||
}
|
||||
db, err := c.repl.store.GetDatabase(c.repl.currentDB)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, coll := range db.ListCollections() {
|
||||
if strings.HasPrefix(coll, prefix) {
|
||||
out = append(out, coll)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *Completer) completeCollectionThenField(cmdName, lastArg string) []string {
|
||||
if c.repl.store == nil || c.repl.currentDB == "" {
|
||||
return c.completeCollections(lastArg)
|
||||
}
|
||||
|
||||
db, err := c.repl.store.GetDatabase(c.repl.currentDB)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
colls := db.ListCollections()
|
||||
for _, coll := range colls {
|
||||
if coll == lastArg {
|
||||
return c.completeFields(coll, "")
|
||||
}
|
||||
}
|
||||
|
||||
if cols := c.completeCollections(lastArg); len(cols) > 0 {
|
||||
return cols
|
||||
}
|
||||
|
||||
if len(colls) > 0 {
|
||||
return c.completeFields(colls[0], lastArg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Completer) completeFields(collName, prefix string) []string {
|
||||
if c.repl.store == nil || c.repl.currentDB == "" {
|
||||
return nil
|
||||
}
|
||||
db, err := c.repl.store.GetDatabase(c.repl.currentDB)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fieldSet := map[string]struct{}{}
|
||||
for _, doc := range coll.GetAllDocuments() {
|
||||
for k := range doc.GetFields() {
|
||||
fieldSet[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
var out []string
|
||||
for k := range fieldSet {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
out = append(out, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *Completer) completeOperators(prefix string) []string {
|
||||
ops := []string{
|
||||
"BEFORE_INSERT", "AFTER_INSERT",
|
||||
"BEFORE_UPDATE", "AFTER_UPDATE",
|
||||
"BEFORE_DELETE", "AFTER_DELETE",
|
||||
"abort", "skip", "modify", "log", "notify",
|
||||
"unique", "required",
|
||||
"start", "status", "list", "pause", "resume", "cancel", "stats", "config", "queue", "validate",
|
||||
"--description", "--set", "--inc", "--currentDate", "--condition",
|
||||
}
|
||||
var out []string
|
||||
for _, op := range ops {
|
||||
if strings.HasPrefix(op, prefix) {
|
||||
out = append(out, op)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *Completer) completeMigration(prefix string) []string {
|
||||
subs := []string{"start", "status", "list", "pause", "resume", "cancel", "stats", "config", "queue", "validate"}
|
||||
var out []string
|
||||
for _, s := range subs {
|
||||
if strings.HasPrefix(s, prefix) {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// CommonPrefix возвращает общий префикс для набора строк.
|
||||
func CommonPrefix(items []string) string {
|
||||
if len(items) == 0 {
|
||||
return ""
|
||||
}
|
||||
prefix := items[0]
|
||||
for _, s := range items[1:] {
|
||||
for !strings.HasPrefix(s, prefix) {
|
||||
if len(prefix) == 0 {
|
||||
return ""
|
||||
}
|
||||
prefix = prefix[:len(prefix)-1]
|
||||
}
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
// ReadlineCompleter строит статическое дерево команд для readline.
|
||||
// Динамические имена коллекций/полей дополняются через DynamicCompleter
|
||||
// (см. ReadlineDynamicFunc ниже), чтобы не пересобирать дерево на каждый ввод.
|
||||
func (c *Completer) ReadlineCompleter() *readline.PrefixCompleter {
|
||||
items := []readline.PrefixCompleterInterface{}
|
||||
|
||||
// Сортируем имена команд для стабильного вывода.
|
||||
names := make([]string, 0, len(c.repl.commands))
|
||||
for name := range c.repl.commands {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for _, name := range names {
|
||||
items = append(items, readline.PcItem(name))
|
||||
}
|
||||
|
||||
// Системные команды.
|
||||
items = append(items, readline.PcItem("help"))
|
||||
items = append(items, readline.PcItem("clear"))
|
||||
items = append(items, readline.PcItem("quit"))
|
||||
items = append(items, readline.PcItem("exit"))
|
||||
items = append(items, readline.PcItem("pager",
|
||||
readline.PcItem("on"),
|
||||
readline.PcItem("off"),
|
||||
))
|
||||
|
||||
return readline.NewPrefixCompleter(items...)
|
||||
}
|
||||
|
||||
// ReadlineDynamicFunc возвращает функцию для readline.Config.DynamicComplete.
|
||||
// Она вызывается на каждый Tab и позволяет дополнять коллекции и поля.
|
||||
func (c *Completer) ReadlineDynamicFunc() func(string, int) []string {
|
||||
return func(line string, pos int) []string {
|
||||
prefix := ""
|
||||
if pos <= len(line) {
|
||||
prefix = line[:pos]
|
||||
}
|
||||
return c.Complete(prefix)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/repl/pager.go
|
||||
// Назначение: Пейджер для постраничного вывода длинных результатов.
|
||||
// Использует внешний pager (less/more) если доступен, иначе встроенный.
|
||||
// Совместимо с Linux и OpenIndiana.
|
||||
|
||||
package repl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Pager предоставляет постраничный вывод.
|
||||
type Pager struct {
|
||||
enabled bool
|
||||
threshold int
|
||||
external string
|
||||
lines int
|
||||
}
|
||||
|
||||
// NewPager создаёт пейджер.
|
||||
func NewPager(enabled bool, threshold int) *Pager {
|
||||
p := &Pager{
|
||||
enabled: enabled,
|
||||
threshold: threshold,
|
||||
lines: 24,
|
||||
}
|
||||
p.external = detectPager()
|
||||
return p
|
||||
}
|
||||
|
||||
// detectPager ищет доступный внешний pager.
|
||||
// Порядок: $PAGER -> less -> more -> "" (встроенный).
|
||||
func detectPager() string {
|
||||
if env := os.Getenv("PAGER"); env != "" {
|
||||
return env
|
||||
}
|
||||
if path, err := exec.LookPath("less"); err == nil {
|
||||
return path
|
||||
}
|
||||
if path, err := exec.LookPath("more"); err == nil {
|
||||
return path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Enabled возвращает, включён ли пейджер.
|
||||
func (p *Pager) Enabled() bool {
|
||||
return p != nil && p.enabled
|
||||
}
|
||||
|
||||
// SetEnabled включает/выключает пейджер.
|
||||
func (p *Pager) SetEnabled(v bool) {
|
||||
if p != nil {
|
||||
p.enabled = v
|
||||
}
|
||||
}
|
||||
|
||||
// SetThreshold устанавливает порог строк.
|
||||
func (p *Pager) SetThreshold(n int) {
|
||||
if p != nil && n > 0 {
|
||||
p.threshold = n
|
||||
}
|
||||
}
|
||||
|
||||
// Page выводит текст через пейджер, если он длиннее порога.
|
||||
func (p *Pager) Page(text string) error {
|
||||
if p == nil || !p.enabled {
|
||||
fmt.Print(text)
|
||||
return nil
|
||||
}
|
||||
|
||||
lineCount := strings.Count(text, "\n")
|
||||
if lineCount < p.threshold {
|
||||
fmt.Print(text)
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.external != "" {
|
||||
if err := p.pageExternal(text); err == nil {
|
||||
return nil
|
||||
}
|
||||
// fallback на встроенный
|
||||
}
|
||||
|
||||
return p.pageBuiltin(text)
|
||||
}
|
||||
|
||||
// pageExternal запускает внешний pager.
|
||||
func (p *Pager) pageExternal(text string) error {
|
||||
args := []string{}
|
||||
base := strings.ToLower(p.external)
|
||||
|
||||
if strings.Contains(base, "less") {
|
||||
// -R: сохранять ANSI-цвета, -F: выйти если всё влезает, -X: не чистить экран
|
||||
args = append(args, "-RFX")
|
||||
}
|
||||
|
||||
cmd := exec.Command(p.external, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(stdin, text); err != nil {
|
||||
stdin.Close()
|
||||
_ = cmd.Wait()
|
||||
return err
|
||||
}
|
||||
stdin.Close()
|
||||
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
// pageBuiltin — встроенный пейджер (fallback).
|
||||
// Управление: Enter — следующая строка, Space — страница, q — выход.
|
||||
func (p *Pager) pageBuiltin(text string) error {
|
||||
scanner := bufio.NewScanner(strings.NewReader(text))
|
||||
buf := make([]byte, 0, 64*1024)
|
||||
scanner.Buffer(buf, 1024*1024)
|
||||
|
||||
lines := []string{}
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pageSize := p.lines - 1
|
||||
if pageSize < 5 {
|
||||
pageSize = 5
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
pos := 0
|
||||
|
||||
for pos < len(lines) {
|
||||
end := pos + pageSize
|
||||
if end > len(lines) {
|
||||
end = len(lines)
|
||||
}
|
||||
for i := pos; i < end; i++ {
|
||||
fmt.Println(lines[i])
|
||||
}
|
||||
pos = end
|
||||
|
||||
if pos >= len(lines) {
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Printf("\033[7m--More--\033[0m (Enter/Space/q) ")
|
||||
ch, err := reader.ReadByte()
|
||||
fmt.Print("\r\033[K")
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
switch ch {
|
||||
case 'q', 'Q':
|
||||
return nil
|
||||
case '\n', '\r':
|
||||
// одна строка
|
||||
if pos < len(lines) {
|
||||
fmt.Println(lines[pos])
|
||||
pos++
|
||||
}
|
||||
case ' ':
|
||||
// страница
|
||||
default:
|
||||
// игнорируем
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/repl/table.go
|
||||
// Назначение: Табличный вывод результатов (по умолчанию для find/count/stats и т.п.).
|
||||
// Работает без внешних зависимостей, совместимо с Linux и OpenIndiana.
|
||||
|
||||
package repl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Table — простая ASCII-таблица.
|
||||
type Table struct {
|
||||
headers []string
|
||||
rows [][]string
|
||||
}
|
||||
|
||||
// NewTable создаёт таблицу с заголовками.
|
||||
func NewTable(headers ...string) *Table {
|
||||
return &Table{
|
||||
headers: append([]string(nil), headers...),
|
||||
rows: make([][]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// AddRow добавляет строку.
|
||||
func (t *Table) AddRow(cells ...string) {
|
||||
row := make([]string, len(cells))
|
||||
copy(row, cells)
|
||||
t.rows = append(t.rows, row)
|
||||
}
|
||||
|
||||
// Len возвращает число строк.
|
||||
func (t *Table) Len() int {
|
||||
return len(t.rows)
|
||||
}
|
||||
|
||||
// String рендерит таблицу.
|
||||
func (t *Table) String() string {
|
||||
if len(t.headers) == 0 && len(t.rows) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
cols := len(t.headers)
|
||||
for _, r := range t.rows {
|
||||
if len(r) > cols {
|
||||
cols = len(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Нормализуем
|
||||
headers := make([]string, cols)
|
||||
copy(headers, t.headers)
|
||||
|
||||
rows := make([][]string, len(t.rows))
|
||||
for i, r := range t.rows {
|
||||
rr := make([]string, cols)
|
||||
copy(rr, r)
|
||||
rows[i] = rr
|
||||
}
|
||||
|
||||
// Ширины
|
||||
widths := make([]int, cols)
|
||||
for i, h := range headers {
|
||||
widths[i] = displayWidth(h)
|
||||
}
|
||||
for _, r := range rows {
|
||||
for i, c := range r {
|
||||
if w := displayWidth(c); w > widths[i] {
|
||||
widths[i] = w
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
// Разделитель
|
||||
sep := buildSeparator(widths)
|
||||
|
||||
// Заголовок
|
||||
sb.WriteString(sep)
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(renderRow(headers, widths))
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(sep)
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Строки
|
||||
for _, r := range rows {
|
||||
sb.WriteString(renderRow(r, widths))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString(sep)
|
||||
sb.WriteString("\n")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func buildSeparator(widths []int) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("+")
|
||||
for _, w := range widths {
|
||||
sb.WriteString(strings.Repeat("-", w+2))
|
||||
sb.WriteString("+")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func renderRow(cells []string, widths []int) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("|")
|
||||
for i, w := range widths {
|
||||
cell := ""
|
||||
if i < len(cells) {
|
||||
cell = cells[i]
|
||||
}
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(padRight(cell, w))
|
||||
sb.WriteString(" |")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func padRight(s string, w int) string {
|
||||
dw := displayWidth(s)
|
||||
if dw >= w {
|
||||
return s
|
||||
}
|
||||
return s + strings.Repeat(" ", w-dw)
|
||||
}
|
||||
|
||||
// displayWidth учитывает широкие руны (грубо).
|
||||
func displayWidth(s string) int {
|
||||
w := 0
|
||||
for _, r := range s {
|
||||
if r == '\t' {
|
||||
w += 4
|
||||
continue
|
||||
}
|
||||
if r < 32 {
|
||||
continue
|
||||
}
|
||||
if isWide(r) {
|
||||
w += 2
|
||||
} else {
|
||||
w += 1
|
||||
}
|
||||
}
|
||||
_ = utf8.RuneCountInString(s)
|
||||
return w
|
||||
}
|
||||
|
||||
// isWide — грубая эвристика для широких символов CJK.
|
||||
func isWide(r rune) bool {
|
||||
return (r >= 0x1100 && r <= 0x115F) ||
|
||||
(r >= 0x2E80 && r <= 0xA4CF) ||
|
||||
(r >= 0xAC00 && r <= 0xD7A3) ||
|
||||
(r >= 0xF900 && r <= 0xFAFF) ||
|
||||
(r >= 0xFE30 && r <= 0xFE4F) ||
|
||||
(r >= 0xFF00 && r <= 0xFF60) ||
|
||||
(r >= 0xFFE0 && r <= 0xFFE6)
|
||||
}
|
||||
|
||||
// MapToTable преобразует список map в таблицу с сортировкой ключей.
|
||||
func MapToTable(rows []map[string]interface{}) *Table {
|
||||
if len(rows) == 0 {
|
||||
return NewTable()
|
||||
}
|
||||
|
||||
// Собираем все ключи
|
||||
keySet := map[string]struct{}{}
|
||||
for _, r := range rows {
|
||||
for k := range r {
|
||||
keySet[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(keySet))
|
||||
for k := range keySet {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// _id всегда первым, если есть
|
||||
if _, ok := keySet["_id"]; ok {
|
||||
sort.SliceStable(keys, func(i, j int) bool {
|
||||
if keys[i] == "_id" {
|
||||
return true
|
||||
}
|
||||
if keys[j] == "_id" {
|
||||
return false
|
||||
}
|
||||
return keys[i] < keys[j]
|
||||
})
|
||||
}
|
||||
|
||||
t := NewTable(keys...)
|
||||
for _, r := range rows {
|
||||
cells := make([]string, len(keys))
|
||||
for i, k := range keys {
|
||||
v, ok := r[k]
|
||||
if !ok {
|
||||
cells[i] = ""
|
||||
continue
|
||||
}
|
||||
cells[i] = FormatCell(v)
|
||||
}
|
||||
t.AddRow(cells...)
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// FormatCell форматирует значение для ячейки.
|
||||
func FormatCell(v interface{}) string {
|
||||
if v == nil {
|
||||
return "NULL"
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return x
|
||||
case bool:
|
||||
if x {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case float64:
|
||||
if x == float64(int64(x)) {
|
||||
return fmt.Sprintf("%d", int64(x))
|
||||
}
|
||||
return fmt.Sprintf("%g", x)
|
||||
case int, int8, int16, int32, int64:
|
||||
return fmt.Sprintf("%d", x)
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return fmt.Sprintf("%d", x)
|
||||
default:
|
||||
return fmt.Sprintf("%v", x)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user