From 13e60e37af2d4b4f7bb9f0617b1479252d161823 Mon Sep 17 00:00:00 2001 From: awatercolorpen Date: Mon, 20 Apr 2026 14:55:24 +0800 Subject: [PATCH] docs: add usage examples and architecture guide (Phase 2) - docs/examples.md: 7 real-world usage scenarios * Simple single-table ClickHouse query * Time-range filter with TimeInterval * Custom metrics formula (composition / ratio) * Multi-table JOIN (fact_dimension_join) query * RunSync vs RunChan (sync vs streaming) * Debug: BuildSQL without executing * SQLite quick-start for local development - docs/architecture.md: contributor-focused architecture guide * Component responsibilities (Manager, Dictionary, Translator, Clause, Clients, Result) * Full request lifecycle walkthrough * Data-flow diagram * Package layout table * Key design decisions and extension points - spec: mark examples and architecture tasks as done --- docs/architecture.md | 271 ++++++++++- docs/examples.md | 459 +++++++++++++++++- .../2026-03-11-olap-sql-modernization.md | 4 +- 3 files changed, 712 insertions(+), 22 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index a41e544..701d177 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,22 +1,267 @@ # Architecture -> **Work in progress.** This file is a placeholder — a full architecture deep-dive will be added in a follow-up PR. +This document describes the internal design of olap-sql for contributors who want to understand, extend, or debug the library. + +--- + +## Table of Contents + +- [High-level overview](#high-level-overview) +- [Component responsibilities](#component-responsibilities) + - [Configuration & Manager](#configuration--manager) + - [Dictionary & Adapter](#dictionary--adapter) + - [Translator](#translator) + - [Clause & Statement](#clause--statement) + - [Clients & Database layer](#clients--database-layer) + - [Result](#result) +- [Request lifecycle](#request-lifecycle) +- [Data-flow diagram](#data-flow-diagram) +- [Package layout](#package-layout) +- [Key design decisions](#key-design-decisions) +- [Extension points](#extension-points) + +--- ## High-level overview -olap-sql is organized around four core abstractions: +olap-sql sits between your application code and your OLAP database. +You describe your data model once in a TOML schema file. After that, your code only works with high-level query objects — no SQL strings, no JOIN logic, no database-specific dialect. + +``` +Your code + │ types.Query + ▼ +Manager ──► Dictionary ──► Translator ──► types.Clause + │ SQL string / *gorm.DB + ▼ + Clients ──► Database + │ []map[string]any rows + ▼ + types.Result +``` + +--- + +## Component responsibilities + +### Configuration & Manager + +`Configuration` is the single struct you pass to `NewManager`. It carries two independent sections: + +| Field | Purpose | +|--------------------|---------| +| `ClientsOption` | Maps string keys → `DBOption` (DSN + type). Drives how GORM connections are opened. | +| `DictionaryOption` | Points to the TOML schema file (or another adapter). Drives schema loading. | + +`Manager` is the public entry point. It owns a `Clients` map and a `Dictionary`, and exposes four methods: + +| Method | Description | +|----------------------|-------------| +| `BuildSQL(query)` | Translate query → SQL string (dry-run, no DB call) | +| `BuildTransaction(q)`| Translate query → `*gorm.DB` ready to execute | +| `RunSync(query)` | Execute and return all rows in a slice | +| `RunChan(query)` | Execute and stream rows over a channel | + +### Dictionary & Adapter + +`Dictionary` wraps an `IAdapter`. The adapter is responsible for loading and serving the virtual schema: sets, sources, metrics, and dimensions. + +``` +Dictionary + └── IAdapter (interface) + └── fileAdapter ← current implementation; loads from a TOML file +``` + +`IAdapter` exposes: + +- `GetDataSetByKey(name)` — returns the named `Set` with its configured DB type and data source +- `GetSourceByKey(name)` — returns a `Source` (table or join definition) +- `GetMetricsByKey(name)` — returns a `Metric` definition +- `GetDimensionByKey(name)` — returns a `Dimension` definition + +New adapter types (e.g. load schema from a database, HTTP endpoint, or in-memory struct) can be added by implementing `IAdapter`. + +### Translator + +`Translator` converts a `types.Query` into a `types.Clause`. This is where the core query-building logic lives. + +The translation steps are: + +1. **Set resolution** — find the `Set` named by `query.DataSetName`; determine the target DB type +2. **Metric expansion** — for each metric name, resolve its definition (and recursively resolve composition dependencies) +3. **Dimension expansion** — same for dimensions +4. **Filter translation** — convert `Filter` structs to SQL `WHERE` fragments; handle tree operators (`AND`, `OR`) recursively +5. **ORDER BY / LIMIT** — append ordering and pagination clauses +6. **JOIN resolution** — if the source is a `fact_dimension_join` or `merge_join`, synthesise the necessary `JOIN ON` expressions +7. **Clause assembly** — package everything into a `Clause` that knows its DB type, dataset, and all SQL fragments + +The translator is created via `NewTranslator(*TranslatorOption)` and is an internal type; callers use `Dictionary.Translate(query)`. + +### Clause & Statement + +`types.Clause` is the output of the Translator. It is a database-backend-specific object that wraps the translated SQL fragments and knows how to apply them to a `*gorm.DB`. + +```go +type Clause interface { + GetDBType() DBType + GetDataset() string + BuildDB(db *gorm.DB) (*gorm.DB, error) + BuildSQL(db *gorm.DB) (string, error) +} +``` + +Each SQL fragment (SELECT column, WHERE condition, ORDER BY, etc.) implements the `Statement` interface: + +```go +type Statement interface { + Expression() (string, error) + Alias() (string, error) + Statement() (string, error) +} +``` + +This makes it straightforward to add new metric types or filter operators: implement `Statement`, register the type constant, and wire it in the translator. + +### Clients & Database layer + +`Clients` is a `map[string]*gorm.DB` keyed by `""` or `"/"`. + +- `RegisterByOption` opens connections from `ClientsOption` at startup +- `Get(dbType, dataset)` performs a two-level lookup (dataset-specific → type-level fallback) +- `BuildDB(clause)` and `BuildSQL(clause)` are convenience wrappers that pick the right connection and execute the clause + +GORM is used as the SQL builder and connection pool. Supported drivers: ClickHouse, MySQL, PostgreSQL, SQLite. + +### Result + +`types.Result` is the output handed back to callers: + +```go +type Result struct { + Dimensions []string // ordered list of column names (dimensions + metrics) + Source []map[string]any // one map per row; keys are column names +} +``` + +`SetDimensions(query)` copies `query.Dimensions` then appends `query.Metrics`, preserving the caller's column order. Each row in `Source` maps a column name to its raw value as returned by the GORM scanner. + +--- + +## Request lifecycle + +``` +manager.RunSync(query) + │ + ├─ query.TranslateTimeIntervalToFilter() // expand TimeInterval → Filter + │ + ├─ dictionary.Translate(query) + │ ├─ adapter.GetDataSetByKey(...) + │ ├─ NewTranslator(option) + │ └─ translator.Translate(query) + │ ├─ resolve metrics + dimensions + │ ├─ translate filters recursively + │ ├─ build JOIN chain (if needed) + │ └─ assemble Clause + │ + ├─ clients.BuildDB(clause) + │ ├─ clients.Get(dbType, dataset) // pick the right *gorm.DB + │ └─ clause.BuildDB(db) // apply SQL fragments to gorm.DB + │ + ├─ RunSync(db) // db.Scan(&rows) + │ + └─ BuildResultSync(query, rows) // wrap rows in Result +``` + +--- + +## Data-flow diagram + +``` + ┌─────────────────────────────────────────────────────────────┐ + │ Your application │ + │ │ + │ query := &types.Query{DataSetName:"wikistat", ...} │ + │ result, err := manager.RunSync(query) │ + └────────────────────────────┬─────────────────────────────────┘ + │ *types.Query + ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ Manager │ + │ │ + │ 1. TranslateTimeIntervalToFilter() │ + │ 2. dictionary.Translate(query) ──────────────────────────► │ + │ Dictionary │ + │ └─ IAdapter │ + │ └─ fileAdapter │ + │ (TOML) │ + │ 3. clients.BuildDB(clause) │ + │ 4. RunSync / RunChan │ + │ 5. BuildResult* │ + └────────────────────────────┬─────────────────────────────────┘ + │ *types.Result + ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ result.Dimensions []string (column names) │ + │ result.Source []map[string]any (rows) │ + └─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Package layout + +``` +olap-sql/ +├── *.go # Public API: Manager, Configuration, Clients, Dictionary, ... +├── api/ +│ └── types/ # Shared type definitions (Query, Filter, Result, Clause, ...) +├── test/ +│ ├── dictionary.ck.toml # ClickHouse test schema +│ └── dictionary.sqlite.toml # SQLite test schema +├── docs/ # Documentation (you are here) +└── scripts/ # Utility shell scripts +``` + +| File / package | Responsibility | +|----------------|----------------| +| `manager.go` | Public `Manager` type; `RunSync`, `RunChan`, `BuildSQL`, `BuildTransaction` | +| `client.go` | `Clients` map; `RegisterByOption`, `Get`, `BuildDB`, `BuildSQL` | +| `configuration.go` | `Configuration` struct | +| `database.go` | `DBOption`; opens GORM connections by `DBType` | +| `dictionary.go`| `Dictionary`; wraps `IAdapter`; exposes `Translate` | +| `dictionary_adapter.go` | `IAdapter` interface; `fileAdapter` implementation | +| `dictionary_column.go` | Column-level schema objects (metric/dimension field descriptors) | +| `dictionary_translator.go` | `Translator`; converts `Query` → `Clause` | +| `dictionary_splitter.go` | Splits a joined source into its constituent tables and JOIN keys | +| `run.go` | `RunSync`, `RunChan`, `BuildResult*` helpers | +| `dependency_graph.go` | Resolves composition metric/dimension dependency chains | +| `api/types/` | All shared value types: `Query`, `Filter`, `Clause`, `Result`, `Statement`, ... | + +--- + +## Key design decisions + +**Schema-driven, not code-driven.** +The TOML schema is the single source of truth for which tables exist, how they join, and what each metric means. Application code only references names (strings). This means you can change the underlying table structure without touching Go code. + +**Composition by name reference.** +Derived metrics (`METRIC_DIVIDE`, `METRIC_ADD`, etc.) reference their inputs by `"."` strings. The translator resolves these recursively, which allows arbitrarily deep derivation chains. + +**GORM as a dialect bridge.** +Rather than hand-rolling SQL for each database, olap-sql builds a `*gorm.DB` with `Select`, `Where`, `Joins`, `Group`, `Order`, `Limit` calls. GORM translates these to dialect-appropriate SQL, giving olap-sql ClickHouse / MySQL / Postgres / SQLite support for free. -| Component | Responsibility | -|-----------|----------------| -| **Schema / Configuration** | Declares the virtual schema: sets, sources, dimensions, metrics, and joins | -| **Manager** | Loads configuration and owns the compiled schema at runtime | -| **Query** | Describes what data the caller wants (filters, grouping, ordering, pagination) | -| **Result** | Delivers the query output as structured rows | +**Flat result model.** +`Result.Source` is `[]map[string]any` — no generated structs, no reflection-based mapping. This trades a small runtime cost for zero code generation and easy JSON serialisation. -See [Getting Started](./getting-started.md) for a walkthrough of how these fit together. +--- -## Coming soon +## Extension points -- Component interaction diagrams -- Dependency graph design -- Extension points and custom adapters +| What to extend | Where to look | +|----------------|---------------| +| New database backend | Add a case to `getDialect` in `database.go`; add the GORM driver dependency in `go.mod` | +| New metric type | Add a constant in `api/types/metric.go`; handle it in the translator and the `Statement` implementation | +| New dimension type | Same pattern as metric types | +| New filter operator | Add a constant in `api/types/filter.go`; handle it in `Filter.Expression()` | +| Load schema from DB/API | Implement `IAdapter` in `dictionary_adapter.go` | +| Custom SQL fragment | Implement `types.Statement` and return it from the translator | diff --git a/docs/examples.md b/docs/examples.md index 7ba345d..ffcec0f 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -1,12 +1,457 @@ # Examples -> **Work in progress.** This file is a placeholder — detailed usage examples will be added in a follow-up PR. +This page shows common real-world usage patterns for olap-sql. +All examples use **ClickHouse** unless stated otherwise; the query syntax is identical for other backends — only the DSN and a few DB-specific SQL functions differ. -For now, see the inline code snippets in [Getting Started](./getting-started.md) and the test files in the repository root for practical usage patterns. +--- -## Coming soon +## Table of Contents -- SQLite quick-start example -- ClickHouse multi-tenant example -- Filtering, ordering, and pagination patterns -- Custom dictionary adapters +- [1. Simple single-table query (ClickHouse)](#1-simple-single-table-query-clickhouse) +- [2. Time-range filter with `time_interval`](#2-time-range-filter-with-time_interval) +- [3. Custom metrics formula (division / ratio)](#3-custom-metrics-formula-division--ratio) +- [4. Multi-table JOIN query](#4-multi-table-join-query) +- [5. Synchronous vs streaming (RunSync vs RunChan)](#5-synchronous-vs-streaming-runsync-vs-runchan) +- [6. Debug: inspect generated SQL without running it](#6-debug-inspect-generated-sql-without-running-it) +- [7. SQLite quick-start (no ClickHouse needed)](#7-sqlite-quick-start-no-clickhouse-needed) + +--- + +## 1. Simple single-table query (ClickHouse) + +The simplest case: one metric, one dimension, no filters. + +**TOML schema** (`olap.toml`): + +```toml +sets = [ + {name = "wikistat", type = "clickhouse", data_source = "wikistat"}, +] + +sources = [ + {database = "", name = "wikistat", type = "fact"}, +] + +metrics = [ + {data_source = "wikistat", type = "METRIC_SUM", name = "hits", field_name = "hits", value_type = "VALUE_INTEGER"}, +] + +dimensions = [ + {data_source = "wikistat", type = "DIMENSION_SINGLE", name = "date", field_name = "date", value_type = "VALUE_STRING"}, + {data_source = "wikistat", type = "DIMENSION_SINGLE", name = "project", field_name = "project", value_type = "VALUE_STRING"}, +] +``` + +**Go code**: + +```go +package main + +import ( + "fmt" + "log" + + olapsql "github.com/awatercolorpen/olap-sql" + "github.com/awatercolorpen/olap-sql/api/types" +) + +func main() { + cfg := &olapsql.Configuration{ + ClientsOption: map[string]*olapsql.DBOption{ + "clickhouse": { + DSN: "clickhouse://localhost:9000/default", + Type: types.DBTypeClickHouse, + }, + }, + DictionaryOption: &olapsql.Option{ + AdapterOption: olapsql.AdapterOption{Dsn: "olap.toml"}, + }, + } + + manager, err := olapsql.NewManager(cfg) + if err != nil { + log.Fatal(err) + } + + query := &types.Query{ + DataSetName: "wikistat", + Metrics: []string{"hits"}, + Dimensions: []string{"project"}, + } + + result, err := manager.RunSync(query) + if err != nil { + log.Fatal(err) + } + + fmt.Println("columns:", result.Dimensions) + for _, row := range result.Source { + fmt.Println(row) + } +} +``` + +**Generated SQL**: + +```sql +SELECT + wikistat.project AS project, + 1.0 * SUM(wikistat.hits) AS hits +FROM wikistat AS wikistat +GROUP BY wikistat.project +``` + +--- + +## 2. Time-range filter with `time_interval` + +`TimeInterval` is a shorthand for a pair of `>=` / `<` filters on a timestamp/date column. +It is automatically expanded into the `WHERE` clause. + +```go +query := &types.Query{ + DataSetName: "wikistat", + TimeInterval: &types.TimeInterval{ + Name: "date", // must match a declared dimension name + Start: "2021-05-06", // inclusive + End: "2021-05-08", // exclusive + }, + Metrics: []string{"hits"}, + Dimensions: []string{"date"}, +} +``` + +**Generated SQL**: + +```sql +SELECT + wikistat.date AS date, + 1.0 * SUM(wikistat.hits) AS hits +FROM wikistat AS wikistat +WHERE (wikistat.date >= '2021-05-06' AND wikistat.date < '2021-05-08') +GROUP BY wikistat.date +``` + +You can also add extra filters on top of the time interval: + +```go +query := &types.Query{ + DataSetName: "wikistat", + TimeInterval: &types.TimeInterval{ + Name: "date", + Start: "2021-05-01", + End: "2021-06-01", + }, + Metrics: []string{"hits"}, + Dimensions: []string{"date", "project"}, + Filters: []*types.Filter{ + { + OperatorType: types.FilterOperatorTypeIn, + Name: "project", + Value: []any{"en", "de", "fr"}, + }, + }, + Orders: []*types.OrderBy{ + {Name: "hits", Direction: types.OrderDirectionTypeDescending}, + }, + Limit: &types.Limit{Limit: 10}, +} +``` + +**Generated SQL**: + +```sql +SELECT + wikistat.date AS date, + wikistat.project AS project, + 1.0 * SUM(wikistat.hits) AS hits +FROM wikistat AS wikistat +WHERE + (wikistat.date >= '2021-05-01' AND wikistat.date < '2021-06-01') + AND wikistat.project IN ('en', 'de', 'fr') +GROUP BY wikistat.date, wikistat.project +ORDER BY hits DESC +LIMIT 10 +``` + +--- + +## 3. Custom metrics formula (division / ratio) + +olap-sql supports **composition metrics** — derived metrics built from other metrics. +A common pattern is a ratio like `hits_avg = hits / count`. + +**TOML schema**: + +```toml +metrics = [ + # Base metrics + {data_source = "wikistat", type = "METRIC_SUM", name = "hits", field_name = "hits", value_type = "VALUE_INTEGER"}, + {data_source = "wikistat", type = "METRIC_COUNT", name = "count", field_name = "*", value_type = "VALUE_INTEGER"}, + {data_source = "wikistat", type = "METRIC_SUM", name = "size_sum", field_name = "size", value_type = "VALUE_INTEGER"}, + + # Derived (composition) metrics + # hits_avg = hits / count (average hits per row) + {data_source = "wikistat", type = "METRIC_DIVIDE", name = "hits_avg", value_type = "VALUE_FLOAT", + dependency = ["wikistat.hits", "wikistat.count"]}, + + # size_per_hit = size_sum / hits + {data_source = "wikistat", type = "METRIC_DIVIDE", name = "size_per_hit", value_type = "VALUE_FLOAT", + dependency = ["wikistat.size_sum", "wikistat.hits"]}, +] +``` + +**Query**: + +```go +query := &types.Query{ + DataSetName: "wikistat", + Metrics: []string{"hits", "hits_avg", "size_per_hit"}, + Dimensions: []string{"date"}, +} +``` + +**Generated SQL**: + +```sql +SELECT + wikistat.date AS date, + 1.0 * SUM(wikistat.hits) AS hits, + (1.0 * SUM(wikistat.hits)) / NULLIF(COUNT(*), 0) AS hits_avg, + (1.0 * SUM(wikistat.size)) / NULLIF((1.0 * SUM(wikistat.hits)), 0) AS size_per_hit +FROM wikistat AS wikistat +GROUP BY wikistat.date +``` + +Other supported composition operators: + +| TOML type | SQL operator | Example | +|-------------------|----------------|---------------------------------------------| +| `METRIC_ADD` | `+` | `dependency = ["wikistat.a", "wikistat.b"]` | +| `METRIC_SUBTRACT` | `-` | `dependency = ["wikistat.a", "wikistat.b"]` | +| `METRIC_MULTIPLY` | `*` | `dependency = ["wikistat.a", "wikistat.b"]` | +| `METRIC_DIVIDE` | `/ NULLIF(,0)` | `dependency = ["wikistat.a", "wikistat.b"]` | + +--- + +## 4. Multi-table JOIN query + +olap-sql can join a **fact table** to one or more **dimension tables** automatically. +You define the join keys in the TOML schema; the generated SQL handles the `JOIN ON`. + +### Schema + +```toml +sets = [ + {name = "wikistat_join", type = "clickhouse", data_source = "wikistat_base"}, +] + +sources = [ + # Fact table + {database = "", name = "wikistat", type = "fact"}, + # Dimension tables + {database = "", name = "wikistat_relate", type = "dimension"}, + {database = "", name = "wikistat_class", type = "dimension"}, + # Virtual joined source: wikistat ⟶ wikistat_relate ⟶ wikistat_class + {database = "", name = "wikistat_base", type = "fact_dimension_join", dimension_join = [ + [ + {data_source = "wikistat", dimension = ["project"]}, + {data_source = "wikistat_relate", dimension = ["project"]}, + ], + [ + {data_source = "wikistat_relate", dimension = ["class_id"]}, + {data_source = "wikistat_class", dimension = ["class_id"]}, + ], + ]}, +] + +metrics = [ + # Expose fact-table metrics through the joined source + {data_source = "wikistat_base", type = "METRIC_AS", name = "hits", value_type = "VALUE_INTEGER", dependency = ["wikistat.hits"]}, +] + +dimensions = [ + {data_source = "wikistat", type = "DIMENSION_SINGLE", name = "project", field_name = "project", value_type = "VALUE_STRING"}, + {data_source = "wikistat_relate", type = "DIMENSION_SINGLE", name = "project", field_name = "project", value_type = "VALUE_STRING"}, + {data_source = "wikistat_relate", type = "DIMENSION_SINGLE", name = "class_id", field_name = "class", value_type = "VALUE_INTEGER"}, + {data_source = "wikistat_class", type = "DIMENSION_SINGLE", name = "class_id", field_name = "id", value_type = "VALUE_INTEGER"}, + {data_source = "wikistat_class", type = "DIMENSION_SINGLE", name = "class_name", field_name = "name", value_type = "VALUE_STRING"}, + + # Re-export through the joined source + {data_source = "wikistat_base", type = "DIMENSION_MULTI", name = "project", value_type = "VALUE_STRING", dependency = ["wikistat.project", "wikistat_relate.project"]}, + {data_source = "wikistat_base", type = "DIMENSION_MULTI", name = "class_name", value_type = "VALUE_STRING", dependency = ["wikistat_class.class_name"]}, +] +``` + +### Query + +```go +query := &types.Query{ + DataSetName: "wikistat_join", // refers to set name + Metrics: []string{"hits"}, + Dimensions: []string{"project", "class_name"}, +} +``` + +### Generated SQL + +```sql +SELECT + wikistat.project AS project, + wikistat_class.name AS class_name, + 1.0 * SUM(wikistat.hits) AS hits +FROM wikistat AS wikistat +JOIN wikistat_relate AS wikistat_relate + ON wikistat.project = wikistat_relate.project +JOIN wikistat_class AS wikistat_class + ON wikistat_relate.class = wikistat_class.id +GROUP BY wikistat.project, wikistat_class.name +``` + +--- + +## 5. Synchronous vs streaming (RunSync vs RunChan) + +For most queries use `RunSync` — it returns the complete result slice. +For very large result sets (millions of rows), use `RunChan` to process rows as they stream in, avoiding a large in-memory buffer. + +### RunSync (default) + +```go +result, err := manager.RunSync(query) +if err != nil { + log.Fatal(err) +} +// All rows are in result.Source +for _, row := range result.Source { + fmt.Println(row["date"], row["hits"]) +} +``` + +### RunChan (streaming) + +```go +result, err := manager.RunChan(query) +if err != nil { + log.Fatal(err) +} +// result.Source is populated row-by-row as the channel is drained internally. +// Use it the same way after RunChan returns: +for _, row := range result.Source { + fmt.Println(row["date"], row["hits"]) +} +``` + +> **Note**: `RunChan` buffers rows internally the same way as `RunSync` after it returns. +> The real benefit is that rows are fetched from the DB incrementally — the server starts +> sending data before all results are ready, which reduces time-to-first-row on large scans. + +### Choosing between them + +| | `RunSync` | `RunChan` | +|---|---|---| +| API simplicity | ✅ simpler | same after return | +| Memory for large results | loads all rows | fetches incrementally | +| Use case | typical queries | millions of rows / streaming ETL | + +--- + +## 6. Debug: inspect generated SQL without running it + +Use `BuildSQL` to see the SQL that would be executed, without actually running the query. +This is useful for debugging, auditing, or logging. + +```go +sql, err := manager.BuildSQL(query) +if err != nil { + log.Fatal(err) +} +fmt.Println("Generated SQL:\n", sql) +``` + +You can also enable GORM debug logging on all connections: + +```go +import "gorm.io/gorm/logger" + +manager.SetLogger(logger.Default.LogMode(logger.Info)) +``` + +Or create the client with `Debug: true` to log every SQL statement: + +```go +cfg := &olapsql.Configuration{ + ClientsOption: map[string]*olapsql.DBOption{ + "clickhouse": { + DSN: "clickhouse://localhost:9000/default", + Type: types.DBTypeClickHouse, + Debug: true, // ← prints SQL to stdout + }, + }, + // ... +} +``` + +--- + +## 7. SQLite quick-start (no ClickHouse needed) + +The test suite uses SQLite so you can experiment locally without any external database. + +```go +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + + olapsql "github.com/awatercolorpen/olap-sql" + "github.com/awatercolorpen/olap-sql/api/types" +) + +func main() { + dbPath := filepath.Join(os.TempDir(), "demo.db") + + cfg := &olapsql.Configuration{ + ClientsOption: map[string]*olapsql.DBOption{ + "sqlite": { + DSN: dbPath, + Type: types.DBTypeSQLite, + }, + }, + DictionaryOption: &olapsql.Option{ + AdapterOption: olapsql.AdapterOption{ + Type: olapsql.FILEAdapter, + Dsn: "test/dictionary.sqlite.toml", // included in the repo + }, + }, + } + + manager, err := olapsql.NewManager(cfg) + if err != nil { + log.Fatal(err) + } + + query := &types.Query{ + DataSetName: "wikistat", + TimeInterval: &types.TimeInterval{ + Name: "date", + Start: "2021-05-06", + End: "2021-05-08", + }, + Metrics: []string{"hits"}, + Dimensions: []string{"date"}, + Orders: []*types.OrderBy{{Name: "date", Direction: types.OrderDirectionTypeAscending}}, + } + + sql, err := manager.BuildSQL(query) + if err != nil { + log.Fatal(err) + } + fmt.Println("SQL:", sql) +} +``` + +See `test/dictionary.sqlite.toml` in the repository for a complete working TOML schema, and the `*_test.go` files for more query examples. diff --git a/docs/superpowers/specs/2026-03-11-olap-sql-modernization.md b/docs/superpowers/specs/2026-03-11-olap-sql-modernization.md index e03908c..6bcacf2 100644 --- a/docs/superpowers/specs/2026-03-11-olap-sql-modernization.md +++ b/docs/superpowers/specs/2026-03-11-olap-sql-modernization.md @@ -53,12 +53,12 @@ olap-sql 是一个 Go 的 OLAP 查询 SQL 生成库,支持 metrics、dimension - [x] 重写 Getting Started(step-by-step,有完整可运行代码) - [ ] API 文档:每个方法说明参数、返回值、使用场景 -- [ ] 增加常见使用场景示例: +- [x] 增加常见使用场景示例: - ClickHouse 多表联查 - 自定义 metrics 公式 - 时间区间过滤 - 并发查询 vs 同步查询 -- [ ] 架构说明文档(给贡献者看) +- [x] 架构说明文档(给贡献者看) - [ ] CONTRIBUTING.md **卡点:完成后汇报,等用户确认进入 Phase 3**