strata-cli 0.1.6.beta → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +27 -9
- data/lib/strata/cli/ai/configuration.rb +3 -3
- data/lib/strata/cli/ai/services/table_generator.rb +39 -11
- data/lib/strata/cli/credentials.rb +7 -8
- data/lib/strata/cli/generators/project.rb +6 -0
- data/lib/strata/cli/generators/templates/AGENTS.md +136 -0
- data/lib/strata/cli/generators/templates/adapters/databricks.yml +46 -0
- data/lib/strata/cli/generators/templates/adapters/snowflake.yml +1 -21
- data/lib/strata/cli/generators/templates/rel.domain.yml +7 -7
- data/lib/strata/cli/sub_commands/datasource.rb +17 -3
- data/lib/strata/cli/sub_commands/deploy.rb +27 -4
- data/lib/strata/cli/utils/archive.rb +15 -6
- data/lib/strata/cli/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e87ebe6383ea4dea3c2b0a57d47bc8a70cdbad507bcb9233614fb33161f5e71a
|
|
4
|
+
data.tar.gz: c343df7299ff6959a73e31e68ca3cc959c5e9596dceb671be4d25868c9c04a5e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1629834279f085c6ef45b74f057f732e722fea8567a98d3ac48e2ae46ce51e0aff46219f41e392940198b5507a8c22d17b20851724c61136ba29d3837f8647b1
|
|
7
|
+
data.tar.gz: 7b37420ff55bdb3910e702e1cb349741d4aa629a67e14b8bb3651189d2d840916a92ac0dbf7e54473ebcc82c71191d3af702a4eb958252a03f97c3919510fd70
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.8] - 2026-04-28
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Snowflake authentication**: Removed OAuth flow from CLI prompts and templates; Snowflake now supports `pat` and `kp` only.
|
|
8
|
+
- **Databricks authentication**: Standardized on OAuth M2M (service principal) only; removed U2M flow and auth-mode selection prompt.
|
|
9
|
+
- **CLI credential UX**: Added explicit guidance during Databricks credential collection that service principal `oauth_client_id` and `oauth_client_secret` are expected.
|
|
10
|
+
|
|
11
|
+
## [0.1.7] - 2026-04-23
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
Added support for Databricks adapter.
|
|
16
|
+
|
|
3
17
|
## [0.1.4.beta] - 2026-02-26
|
|
4
18
|
|
|
5
19
|
### Added
|
data/README.md
CHANGED
|
@@ -80,14 +80,6 @@ strata ds add snowflake # Using alias
|
|
|
80
80
|
### `strata datasource auth DS_KEY`
|
|
81
81
|
Set credentials for a datasource. Credentials stored in `.strata` file.
|
|
82
82
|
|
|
83
|
-
**Options:** `-r, --remote` - Set credentials on remote server
|
|
84
|
-
|
|
85
|
-
**Examples:**
|
|
86
|
-
```bash
|
|
87
|
-
strata datasource auth my_db
|
|
88
|
-
strata ds auth postgres_db --remote
|
|
89
|
-
```
|
|
90
|
-
|
|
91
83
|
### `strata datasource test DS_KEY`
|
|
92
84
|
Test connection to a datasource.
|
|
93
85
|
|
|
@@ -350,18 +342,44 @@ my_snowflake:
|
|
|
350
342
|
schema: PUBLIC
|
|
351
343
|
role: ACCOUNTADMIN
|
|
352
344
|
auth_mode: pat
|
|
345
|
+
|
|
346
|
+
my_databricks:
|
|
347
|
+
adapter: databricks
|
|
348
|
+
name: Databricks Warehouse
|
|
349
|
+
host: workspace.cloud.databricks.com
|
|
350
|
+
warehouse: warehouse_id
|
|
351
|
+
catalog: main
|
|
352
|
+
schema: default
|
|
353
|
+
auth_mode: oauth_m2m
|
|
353
354
|
```
|
|
354
355
|
|
|
356
|
+
### Databricks authentication
|
|
357
|
+
|
|
358
|
+
Databricks now requires explicit `auth_mode`.
|
|
359
|
+
|
|
360
|
+
If you already have Databricks datasources, update `datasources.yml`:
|
|
361
|
+
|
|
362
|
+
```yaml
|
|
363
|
+
my_databricks:
|
|
364
|
+
adapter: databricks
|
|
365
|
+
auth_mode: oauth_m2m
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Use service principal credentials and run `strata ds auth my_databricks` to save `oauth_client_id` and `oauth_client_secret` in `.strata`.
|
|
369
|
+
|
|
355
370
|
## Supported Data Warehouse Adapters
|
|
356
371
|
|
|
357
372
|
- **PostgreSQL** - Full support
|
|
358
373
|
- **MySQL** - Full support
|
|
359
374
|
- **SQL Server** - Full support (including Azure)
|
|
360
|
-
- **Snowflake** - Full support (PAT, Key Pair
|
|
375
|
+
- **Snowflake** - Full support (PAT, Key Pair)
|
|
361
376
|
- **Athena** - AWS Athena support
|
|
362
377
|
- **Trino** - Trino/Presto support
|
|
363
378
|
- **DuckDB** - Embedded analytics database
|
|
364
379
|
- **Druid** - Apache Druid support
|
|
380
|
+
- **Redshift** - Amazon Redshift support
|
|
381
|
+
- **Databricks** - Databricks SQL warehouse support
|
|
382
|
+
- **SQLite** - SQLite database support
|
|
365
383
|
|
|
366
384
|
## Security
|
|
367
385
|
|
|
@@ -25,9 +25,9 @@ module Strata
|
|
|
25
25
|
|
|
26
26
|
def default_model
|
|
27
27
|
case @provider
|
|
28
|
-
when "gemini" then "gemini-2.
|
|
29
|
-
when "openai" then "gpt-
|
|
30
|
-
when "anthropic" then "claude-
|
|
28
|
+
when "gemini" then "gemini-2.5-pro"
|
|
29
|
+
when "openai" then "gpt-5"
|
|
30
|
+
when "anthropic" then "claude-opus-4-6"
|
|
31
31
|
when "mistral" then "mistral-large-latest"
|
|
32
32
|
when "deepseek" then "deepseek-chat"
|
|
33
33
|
else
|
|
@@ -11,18 +11,45 @@ module Strata
|
|
|
11
11
|
# Returns structured JSON for use in the field editor.
|
|
12
12
|
class TableGenerator
|
|
13
13
|
SYSTEM_PROMPT = <<~PROMPT
|
|
14
|
-
You are a semantic modeling expert for
|
|
14
|
+
You are a semantic modeling expert for the Strata Business Intelligence platform.
|
|
15
15
|
Analyze database columns and generate semantic field definitions.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
You must understand that Names are extremely important in the Strata application. When the same named dimension
|
|
18
|
+
is mapped to multiple tables, Strata treats them as the same entity but with multiple potential tables. Strata
|
|
19
|
+
will at query generation time choose the best table. Therefore it is extremely important to consider whether a
|
|
20
|
+
potential dimension or measure is the same as an existing one. You can try and infer that based on the table name,
|
|
21
|
+
column name, and the existing fields. In general, you should use existing names for the same schema type if it makes sense.
|
|
22
|
+
The edge case is when you have a Dimension table like Customer with column first_name and another Dimension
|
|
23
|
+
table called Billed Customer with first_name, then they are likely not the same. A dimension created from
|
|
24
|
+
Billed Customer should be given an appropriately prefixed name: Billed Customer First Name.
|
|
25
|
+
|
|
26
|
+
Next, if the column is something highly ambigous and the current table is likely a dimension table, we should prefix
|
|
27
|
+
then dimension name with approprite prefix based on the name of the dimension table.
|
|
28
|
+
Example:
|
|
29
|
+
table name: item_dim
|
|
30
|
+
column name: color
|
|
31
|
+
dimension name: Item Color
|
|
32
|
+
Example:
|
|
33
|
+
table name: customer
|
|
34
|
+
column name: first name
|
|
35
|
+
dimension name: Customer First Name
|
|
36
|
+
|
|
37
|
+
Given the above for each column, determine:
|
|
18
38
|
- name: Human-friendly field name (e.g., "customer_id" → "Customer ID")
|
|
19
39
|
- description: Brief description of what this field represents
|
|
20
40
|
- schema_type: "dimension" for categorical/text, "measure" for numeric aggregations
|
|
21
|
-
- data_type: string, integer, bigint, decimal, date, date_time, boolean
|
|
41
|
+
- data_type: string, integer, bigint, decimal, date, date_time, boolean. These are the data types supported
|
|
42
|
+
by strata. They do not have precision nor scale. Simply the types listed here. Do not apply any other data types.
|
|
22
43
|
- expression: SQL expression (for measures include aggregation like "sum(amount)")
|
|
23
|
-
- synonyms: Array of
|
|
44
|
+
- synonyms: Array of 0-3 alternative names users might use to refer to this field.
|
|
24
45
|
These help AI search and natural language queries find the right field.
|
|
25
|
-
Example: "
|
|
46
|
+
Example: "Revenue" → ["sales"]
|
|
47
|
+
Example: "Created At" → ["created date", "date"]
|
|
48
|
+
Example: "State" in a geography table → ["province"]
|
|
49
|
+
Example: "Customer Return Date" → [] -- name has high specificity already so no synonyms needed
|
|
50
|
+
|
|
51
|
+
In cases where a dimension already exists (i.e. a dimension with same name exists), omit everything
|
|
52
|
+
except the following fields: name, schema_type, data_type, expression.
|
|
26
53
|
|
|
27
54
|
Output ONLY valid JSON array, no explanations.
|
|
28
55
|
PROMPT
|
|
@@ -163,19 +190,19 @@ module Strata
|
|
|
163
190
|
def load_existing_models_context
|
|
164
191
|
return nil unless Dir.exist?("models")
|
|
165
192
|
|
|
166
|
-
model_files = Dir.glob("models
|
|
193
|
+
model_files = Dir.glob("models/**/tbl.*.yml").first(100) # Limit to 5 for performance
|
|
167
194
|
return nil if model_files.empty?
|
|
168
195
|
|
|
169
|
-
contexts = model_files.
|
|
196
|
+
contexts = model_files.filter_map do |file|
|
|
170
197
|
model = YAML.safe_load_file(file, permitted_classes: [Date, Time], aliases: true) || {}
|
|
171
198
|
fields_summary = (model["fields"] || []).map do |f|
|
|
172
|
-
"#{f["name"]} (#{f["
|
|
173
|
-
end.join("
|
|
174
|
-
"Model: #{model["name"]}
|
|
199
|
+
"#{f["name"]} (#{f["type"]}) data type: #{f["data_type"]}"
|
|
200
|
+
end.join("\n\t")
|
|
201
|
+
"Model: #{model["name"]} \n Fields: \n\t#{fields_summary}"
|
|
175
202
|
rescue => e
|
|
176
203
|
warn "Failed to load model file #{file}: #{e.message}" if ENV["DEBUG"]
|
|
177
204
|
nil
|
|
178
|
-
end
|
|
205
|
+
end
|
|
179
206
|
|
|
180
207
|
contexts.join("\n")
|
|
181
208
|
end
|
|
@@ -276,6 +303,7 @@ module Strata
|
|
|
276
303
|
when /bool/ then "boolean"
|
|
277
304
|
when /timestamp/, /datetime/ then "date_time"
|
|
278
305
|
when /date/ then "date"
|
|
306
|
+
when /char/ then "string"
|
|
279
307
|
else "string"
|
|
280
308
|
end
|
|
281
309
|
end
|
|
@@ -16,8 +16,9 @@ module Strata
|
|
|
16
16
|
|
|
17
17
|
attr_reader :adapter, :credentials
|
|
18
18
|
|
|
19
|
-
def initialize(adapter)
|
|
19
|
+
def initialize(adapter, datasource_config: nil)
|
|
20
20
|
@adapter = adapter.downcase.strip
|
|
21
|
+
@datasource_config = datasource_config || {}
|
|
21
22
|
@prompt = TTY::Prompt.new
|
|
22
23
|
end
|
|
23
24
|
|
|
@@ -30,7 +31,7 @@ module Strata
|
|
|
30
31
|
|
|
31
32
|
case adapter
|
|
32
33
|
when "snowflake"
|
|
33
|
-
auth_mode = @prompt.select("Authentication mode:", %w[pat kp
|
|
34
|
+
auth_mode = @prompt.select("Authentication mode:", %w[pat kp], default: "pat")
|
|
34
35
|
credentials["auth_mode"] = auth_mode
|
|
35
36
|
|
|
36
37
|
case auth_mode
|
|
@@ -39,16 +40,14 @@ module Strata
|
|
|
39
40
|
when "kp"
|
|
40
41
|
credentials["username"] = @prompt.ask("Enter Username:")
|
|
41
42
|
credentials["private_key"] = @prompt.ask("Enter Private Key Absolute Path:")
|
|
42
|
-
when "oauth"
|
|
43
|
-
credentials["oauth_client_id"] = @prompt.ask("OAuth Client ID:")
|
|
44
|
-
credentials["oauth_client_secret"] = @prompt.ask("OAuth Client Secret:")
|
|
45
|
-
credentials["oauth_redirect_uri"] = @prompt.ask("OAuth Redirect URI:", default: "https://localhost:3420/callback")
|
|
46
|
-
oauth_scope = @prompt.ask("OAuth Scope (optional):")
|
|
47
|
-
credentials["oauth_scope"] = oauth_scope unless oauth_scope.empty?
|
|
48
43
|
end
|
|
49
44
|
when "athena"
|
|
50
45
|
credentials["access_key_id"] = @prompt.ask("AWS Access Key ID:")
|
|
51
46
|
credentials["secret_access_key"] = @prompt.ask("AWS Secret Access Key:")
|
|
47
|
+
when "databricks"
|
|
48
|
+
credentials["auth_mode"] = "oauth_m2m"
|
|
49
|
+
credentials["oauth_client_id"] = @prompt.ask("OAuth Client ID:")
|
|
50
|
+
credentials["oauth_client_secret"] = @prompt.ask("OAuth Client Secret:")
|
|
52
51
|
else
|
|
53
52
|
if required?
|
|
54
53
|
unless %w[postgres mysql trino sqlserver].include?(adapter)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Strata Semantic Model Project
|
|
2
|
+
|
|
3
|
+
This is a **Strata** semantic model project. It defines the dimensions, measures, and relationships that the Strata BI platform exposes to end users for querying and reporting.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
project.yml # Project config (server URL, project ID, production branch)
|
|
9
|
+
datasources.yml # Data warehouse connection config (no credentials)
|
|
10
|
+
.strata # Local credentials and API keys — gitignored, never commit
|
|
11
|
+
models/ # Semantic model definitions
|
|
12
|
+
tbl.*.yml # Table models (dimensions + measures)
|
|
13
|
+
rel.*.yml # Relationship/join definitions
|
|
14
|
+
tests/ # Model tests
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## CLI Reference
|
|
18
|
+
|
|
19
|
+
Use the `strata` CLI to manage this project. All commands should be run from the project root.
|
|
20
|
+
|
|
21
|
+
### Inspect
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
strata table list # List all defined semantic tables
|
|
25
|
+
strata datasource list # List configured datasources
|
|
26
|
+
strata datasource tables [DS_KEY] # Browse physical tables in a datasource
|
|
27
|
+
strata datasource meta DS_KEY TABLE_NAME # Show columns for a physical table
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Create
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
strata create table [TABLE_PATH] # AI-assisted semantic table creation
|
|
34
|
+
# TABLE_PATH examples:
|
|
35
|
+
# orders → models/tbl.orders.yml
|
|
36
|
+
# sales/orders → models/sales/tbl.orders.yml
|
|
37
|
+
# dw.fact_orders → models/tbl.dw.fact_orders.yml (schema-prefixed)
|
|
38
|
+
|
|
39
|
+
strata create relation RELATION_NAME # Create a join definition (rel.*.yml)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`strata create table` fetches column metadata from the datasource, generates field definitions via AI, then opens an interactive editor to review and confirm before writing the YAML file.
|
|
43
|
+
|
|
44
|
+
### Validate & Deploy
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
strata audit all # Run all validation checks
|
|
48
|
+
strata audit syntax # Validate YAML syntax only
|
|
49
|
+
strata audit models # Validate model structure and references
|
|
50
|
+
strata audit connections # Test all datasource connections
|
|
51
|
+
|
|
52
|
+
strata deploy # Deploy to Strata server
|
|
53
|
+
strata deploy -e production # Deploy to a specific environment
|
|
54
|
+
strata deploy status # Check current deployment status
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Semantic Model Files
|
|
58
|
+
|
|
59
|
+
### Table model — `models/tbl.<name>.yml`
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
datasource: "<datasource_key_or_name>" # Required
|
|
63
|
+
name: "<logical_name>" # Required — unique within datasource
|
|
64
|
+
physical_name: "<db_table_name>" # Required — physical table in the warehouse
|
|
65
|
+
cost: 10 # Lower = preferred when multiple tables can answer a query
|
|
66
|
+
|
|
67
|
+
# Optional
|
|
68
|
+
description: ""
|
|
69
|
+
snapshot_date: "<dimension_name>" # For snapshot/inventory tables only
|
|
70
|
+
tags: []
|
|
71
|
+
imports: # Inherit fields from other YAML files
|
|
72
|
+
- "../shared/common_fields.yml"
|
|
73
|
+
|
|
74
|
+
fields:
|
|
75
|
+
- type: dimension # "dimension" (categorical) or "measure" (numeric aggregate)
|
|
76
|
+
name: "Order ID" # Required — human-friendly name; MUST be consistent across tables
|
|
77
|
+
description: ""
|
|
78
|
+
data_type: bigint # See supported types below
|
|
79
|
+
expression: order_id # SQL column name, or object form (see below)
|
|
80
|
+
synonyms: [] # 0–3 alternative names for AI search
|
|
81
|
+
|
|
82
|
+
- type: measure
|
|
83
|
+
name: "Total Revenue"
|
|
84
|
+
data_type: decimal
|
|
85
|
+
expression: "sum(amount)" # Measures use SQL aggregation expressions
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Supported `data_type` values:** `string`, `integer`, `bigint`, `decimal`, `date`, `date_time`, `boolean`
|
|
89
|
+
|
|
90
|
+
No precision or scale — just the base type.
|
|
91
|
+
|
|
92
|
+
**Complex `expression` form:**
|
|
93
|
+
```yaml
|
|
94
|
+
expression:
|
|
95
|
+
sql: my_column
|
|
96
|
+
primary_key: true # optional
|
|
97
|
+
lookup: true # optional
|
|
98
|
+
array: true # optional
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Date/time grains** (optional, for `date`/`date_time` fields):
|
|
102
|
+
```yaml
|
|
103
|
+
grains: [day, week, month, quarter, year]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Relationship model — `models/rel.<name>.yml`
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
datasource: "<datasource_key_or_name>"
|
|
110
|
+
|
|
111
|
+
order_customer: # Relationship name (any key)
|
|
112
|
+
left: "Orders" # Table on the "many" side
|
|
113
|
+
right: "Customers" # Table on the "one" side
|
|
114
|
+
sql: "orders.customer_id = customers.id"
|
|
115
|
+
cardinality: many_to_one # one_to_one | one_to_many | many_to_one | many_to_many
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Important Naming Rules
|
|
119
|
+
|
|
120
|
+
- **Dimension names are global.** When the same dimension name appears on multiple tables, Strata treats them as the same concept and picks the best table at query time.
|
|
121
|
+
- **Use consistent names** for shared dimensions (e.g., "Customer ID", "Order Date") across all table models.
|
|
122
|
+
- **Prefix ambiguous dimensions** from dimension tables: a `color` column on an `item_dim` table should be named `Item Color`, not `Color`.
|
|
123
|
+
|
|
124
|
+
## Workflow
|
|
125
|
+
|
|
126
|
+
1. `strata datasource tables DS_KEY` — explore available physical tables
|
|
127
|
+
2. `strata create table <path>` — generate and review a semantic model
|
|
128
|
+
3. Edit the generated `tbl.*.yml` directly for fine-tuning
|
|
129
|
+
4. `strata audit all` — validate before deploying
|
|
130
|
+
5. `strata deploy` — push to Strata server
|
|
131
|
+
|
|
132
|
+
## Further Reading
|
|
133
|
+
|
|
134
|
+
For advanced semantic model concepts (partitioning, inclusions, exclusions, snapshot measures, display types, formatters, and more), refer to the Strata developer docs:
|
|
135
|
+
|
|
136
|
+
https://strata.do/developer-docs/developer-guide/semantic-model
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Datasource key should be unique in the Project
|
|
2
|
+
<%= @ds_key %>:
|
|
3
|
+
# This is the display name of this datasource
|
|
4
|
+
name: MYDATASOURCENAME
|
|
5
|
+
# Optional description to provide more information in the UI
|
|
6
|
+
description: This DS is the hot tier of our Product Domain
|
|
7
|
+
|
|
8
|
+
# Required. Should be one of hot, warm, or cold. Helps the Query engine
|
|
9
|
+
# prioritize a datasource when multiple can handle the request.
|
|
10
|
+
tier: warm
|
|
11
|
+
|
|
12
|
+
# A valid supported adapter
|
|
13
|
+
adapter: databricks
|
|
14
|
+
|
|
15
|
+
# default client name sent to the db
|
|
16
|
+
client_name: Strata
|
|
17
|
+
|
|
18
|
+
# Max allowed time a query can run on this Datasource before manual time out
|
|
19
|
+
query_timeout: 3600
|
|
20
|
+
|
|
21
|
+
# Required: Databricks workspace host (no scheme), e.g.
|
|
22
|
+
# adb-1234567890123456.7.azuredatabricks.net
|
|
23
|
+
# your-workspace.cloud.databricks.com
|
|
24
|
+
host: workspace-id.cloud.databricks.com
|
|
25
|
+
|
|
26
|
+
# Required: SQL warehouse ID (from SQL Warehouses in the workspace UI, or the API)
|
|
27
|
+
warehouse: warehouse_id_here
|
|
28
|
+
|
|
29
|
+
# Optional: Unity Catalog default catalog (often "main"). Used as default context for
|
|
30
|
+
# the Statement API and for information_schema discovery in the dwh adapter.
|
|
31
|
+
catalog: main
|
|
32
|
+
|
|
33
|
+
# Optional: default schema within that catalog (often "default")
|
|
34
|
+
schema: default
|
|
35
|
+
|
|
36
|
+
# Required: Authentication mode
|
|
37
|
+
# oauth_m2m: service principal (client_credentials)
|
|
38
|
+
auth_mode: oauth_m2m
|
|
39
|
+
#
|
|
40
|
+
# Set credentials securely via:
|
|
41
|
+
# `strata ds auth <%= @ds_key %>`
|
|
42
|
+
# or they are collected when you run `strata datasource add databricks`.
|
|
43
|
+
#
|
|
44
|
+
# For Strata server deploy (datasources synced to the app), use top-level keys
|
|
45
|
+
# oauth_client_id / oauth_client_secret
|
|
46
|
+
# on the datasource record — prefer secrets via the product, not committed YAML.
|
|
@@ -33,15 +33,9 @@
|
|
|
33
33
|
# Optional: Role name
|
|
34
34
|
role: ACCOUNTADMIN
|
|
35
35
|
|
|
36
|
-
# Required: Authentication mode - one of: pat, kp
|
|
36
|
+
# Required: Authentication mode - one of: pat, kp
|
|
37
37
|
# pat: personal_access_token
|
|
38
38
|
# kp: key pair (requires username)
|
|
39
|
-
# oauth: OAuth (requires setup on snowflake to enable)
|
|
40
|
-
# Additional required params
|
|
41
|
-
# oauth_client_id: <MYCLIENTID>
|
|
42
|
-
# oauth_cleint_secret: <MYCLIENTSECRET>
|
|
43
|
-
# oauth_redirect_uri: https://localhost:3420/callback
|
|
44
|
-
# oauth_scope: <SCOPE> # optional
|
|
45
39
|
auth_mode: pat
|
|
46
40
|
|
|
47
41
|
# For Personal Access Token (PAT) authentication:
|
|
@@ -53,17 +47,3 @@
|
|
|
53
47
|
# For Key Pair (KP) authentication, uncomment and set:
|
|
54
48
|
# username: john_doe
|
|
55
49
|
# private_key: /path/to/private_key.pem
|
|
56
|
-
|
|
57
|
-
# For OAuth authentication, additional setup required.
|
|
58
|
-
# https://docs.snowflake.com/en/user-guide/oauth-custom
|
|
59
|
-
#
|
|
60
|
-
# Securely save tokens in .strata by running:
|
|
61
|
-
# `strata ds auth <%= @ds_key %>`
|
|
62
|
-
#
|
|
63
|
-
# This would only work if your snowflake setup allows modification
|
|
64
|
-
# of the redirect url. Otherwise, you can get access_token and
|
|
65
|
-
# refresh_token by other means. Then add it to .strata like so:
|
|
66
|
-
#
|
|
67
|
-
# access_token: wrwerjwrljwer
|
|
68
|
-
# refresh_token: erwerewrwer
|
|
69
|
-
# expires_at: <%= Time.now %>
|
|
@@ -2,26 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
# Required: The datasource these relationships are valid for.
|
|
4
4
|
# The chosen tables will be scoped to those within this datasource.
|
|
5
|
-
# Strata does not support cross datasource
|
|
5
|
+
# Strata does not support cross datasource sqls
|
|
6
6
|
datasource: "<datasource_name>"
|
|
7
7
|
|
|
8
8
|
# Define relationships between tables. The table names used here should
|
|
9
9
|
# correspond to the name given in its respective model file and not the
|
|
10
10
|
# physical name (often they are same).
|
|
11
|
-
# NOTE: Strata does not support many_to_many
|
|
11
|
+
# NOTE: Strata does not support many_to_many sqls.
|
|
12
12
|
|
|
13
13
|
# Example: One-to-many relationship (customer has many orders)
|
|
14
14
|
# customer_orders:
|
|
15
15
|
# left: "customers"
|
|
16
16
|
# right: "orders"
|
|
17
|
-
#
|
|
17
|
+
# sql: "left.id = right.customer_id"
|
|
18
18
|
# cardinality: "one_to_many"
|
|
19
19
|
|
|
20
20
|
# Example: Many-to-one relationship (orders belong to customer)
|
|
21
21
|
# order_customer:
|
|
22
22
|
# left: "orders"
|
|
23
23
|
# right: "customers"
|
|
24
|
-
#
|
|
24
|
+
# sql: "left.customer_id = right.id"
|
|
25
25
|
# cardinality: "many_to_one"
|
|
26
26
|
# # Whether measurs should be aggregated from the low cardinality
|
|
27
27
|
# # table. In most cases this will overcount.
|
|
@@ -31,13 +31,13 @@ datasource: "<datasource_name>"
|
|
|
31
31
|
# user_profile:
|
|
32
32
|
# left: "users"
|
|
33
33
|
# right: "user_profiles"
|
|
34
|
-
#
|
|
34
|
+
# sql: "left.id = right.user_id"
|
|
35
35
|
# cardinality: "one_to_one"
|
|
36
36
|
|
|
37
|
-
# Example: Compound
|
|
37
|
+
# Example: Compound Join
|
|
38
38
|
# user_roles:
|
|
39
39
|
# left: "users"
|
|
40
40
|
# right: "roles"
|
|
41
|
-
#
|
|
41
|
+
# sql: "left.id = right.user_id AND left.id = right.role_id"
|
|
42
42
|
# cardinality: "one_to_many"
|
|
43
43
|
|
|
@@ -86,7 +86,7 @@ module Strata
|
|
|
86
86
|
# Collect adapter-specific fields
|
|
87
87
|
adapter_fields(adapter).each do |field, default_value|
|
|
88
88
|
config[field] = if field == "auth_mode"
|
|
89
|
-
|
|
89
|
+
default_value
|
|
90
90
|
elsif %w[ssl azure].include?(field)
|
|
91
91
|
prompt.yes?(" #{field.tr("_", " ").capitalize}?", default: default_value)
|
|
92
92
|
elsif field == "port"
|
|
@@ -103,11 +103,14 @@ module Strata
|
|
|
103
103
|
say "\n✔ Added #{adapter} config to datasources.yml", :green
|
|
104
104
|
|
|
105
105
|
# Automatically collect credentials if required
|
|
106
|
-
creds = Credentials.new(adapter)
|
|
106
|
+
creds = Credentials.new(adapter, datasource_config: config)
|
|
107
107
|
if creds.required?
|
|
108
108
|
say "\n Now let's set up credentials:\n", :yellow
|
|
109
109
|
say " Note: Credentials are stored securely in the local .strata file", :cyan
|
|
110
110
|
say " and are NOT committed to the repository (ensured by .gitignore).", :cyan
|
|
111
|
+
if adapter == "databricks"
|
|
112
|
+
say " Databricks uses OAuth M2M with service principal credentials (client ID + client secret).", :cyan
|
|
113
|
+
end
|
|
111
114
|
say ""
|
|
112
115
|
creds.collect
|
|
113
116
|
creds.write_local(ds_key, self)
|
|
@@ -137,7 +140,7 @@ module Strata
|
|
|
137
140
|
end
|
|
138
141
|
|
|
139
142
|
adapter = datasources[ds_key]["adapter"]
|
|
140
|
-
creds = Credentials.new(adapter)
|
|
143
|
+
creds = Credentials.new(adapter, datasource_config: datasources[ds_key])
|
|
141
144
|
|
|
142
145
|
unless creds.required?
|
|
143
146
|
say "Credentials not required for #{adapter} adapter.", :yellow
|
|
@@ -147,6 +150,9 @@ module Strata
|
|
|
147
150
|
say "\nEnter credentials for #{ds_key}", :red
|
|
148
151
|
say " Note: Credentials are stored securely in the local .strata file", :cyan
|
|
149
152
|
say " and are NOT committed to the repository (ensured by .gitignore).", :cyan
|
|
153
|
+
if adapter == "databricks"
|
|
154
|
+
say " Databricks uses OAuth M2M with service principal credentials (client ID + client secret).", :cyan
|
|
155
|
+
end
|
|
150
156
|
say ""
|
|
151
157
|
creds.collect
|
|
152
158
|
creds.write_local(ds_key, self)
|
|
@@ -338,6 +344,14 @@ module Strata
|
|
|
338
344
|
"host" => "localhost",
|
|
339
345
|
"port" => 7103
|
|
340
346
|
}
|
|
347
|
+
when "databricks"
|
|
348
|
+
{
|
|
349
|
+
"host" => "workspace-id.cloud.databricks.com",
|
|
350
|
+
"warehouse" => "warehouse_id",
|
|
351
|
+
"catalog" => "main",
|
|
352
|
+
"schema" => "default",
|
|
353
|
+
"auth_mode" => "oauth_m2m"
|
|
354
|
+
}
|
|
341
355
|
else
|
|
342
356
|
{}
|
|
343
357
|
end
|
|
@@ -6,6 +6,7 @@ require_relative "../helpers/color_helper"
|
|
|
6
6
|
require_relative "../helpers/project_helper"
|
|
7
7
|
require_relative "../helpers/description_helper"
|
|
8
8
|
require_relative "../api/client"
|
|
9
|
+
require_relative "../credentials"
|
|
9
10
|
require_relative "../utils"
|
|
10
11
|
require_relative "../utils/archive"
|
|
11
12
|
require_relative "../utils/git"
|
|
@@ -155,7 +156,7 @@ module Strata
|
|
|
155
156
|
q.validate(/\S+/, "Server URL cannot be empty")
|
|
156
157
|
end
|
|
157
158
|
|
|
158
|
-
server = server.strip.
|
|
159
|
+
server = server.strip.delete_suffix("/") # normalize: remove trailing slash
|
|
159
160
|
|
|
160
161
|
with_spinner("Saving server URL to project.yml") do
|
|
161
162
|
save_server_to_project_yml(server)
|
|
@@ -322,6 +323,8 @@ module Strata
|
|
|
322
323
|
end
|
|
323
324
|
|
|
324
325
|
def create_and_upload_archive(last_deployment_commit = nil, refreshed_imports: [])
|
|
326
|
+
file_overrides = datasource_file_overrides
|
|
327
|
+
|
|
325
328
|
if last_deployment_commit && Utils::Git.git_repo?
|
|
326
329
|
# Get changed file paths since last deployment
|
|
327
330
|
changed_paths = Utils::Git.changed_file_paths_since(last_deployment_commit)
|
|
@@ -332,7 +335,7 @@ module Strata
|
|
|
332
335
|
say "To force deploy, run command with --force or -f flag.\n", ColorHelper.info
|
|
333
336
|
exit(0)
|
|
334
337
|
end
|
|
335
|
-
Utils::Archive.create(project_path)
|
|
338
|
+
Utils::Archive.create(project_path, file_overrides: file_overrides)
|
|
336
339
|
else
|
|
337
340
|
# Convert relative paths to absolute paths
|
|
338
341
|
files_to_include = changed_paths.map do |relative_path|
|
|
@@ -343,14 +346,34 @@ module Strata
|
|
|
343
346
|
change_count += refreshed_imports.length if refreshed_imports.any?
|
|
344
347
|
|
|
345
348
|
say "Including #{change_count} changed file(s) in archive...\n", ColorHelper.info
|
|
346
|
-
Utils::Archive.create(project_path, files_to_include: files_to_include)
|
|
349
|
+
Utils::Archive.create(project_path, files_to_include: files_to_include, file_overrides: file_overrides)
|
|
347
350
|
end
|
|
348
351
|
else
|
|
349
352
|
# No last deployment or not a git repo - include all files
|
|
350
|
-
Utils::Archive.create(project_path)
|
|
353
|
+
Utils::Archive.create(project_path, file_overrides: file_overrides)
|
|
351
354
|
end
|
|
352
355
|
end
|
|
353
356
|
|
|
357
|
+
def datasource_file_overrides
|
|
358
|
+
datasources_path = File.join(project_path, "datasources.yml")
|
|
359
|
+
return {} unless File.exist?(datasources_path)
|
|
360
|
+
|
|
361
|
+
datasources = YAML.safe_load_file(datasources_path, permitted_classes: [Date, Time], aliases: true) || {}
|
|
362
|
+
return {} unless datasources.is_a?(Hash)
|
|
363
|
+
|
|
364
|
+
merged_datasources = datasources.transform_values { |config| config.is_a?(Hash) ? config.dup : config }
|
|
365
|
+
datasources.each do |ds_key, config|
|
|
366
|
+
next unless config.is_a?(Hash)
|
|
367
|
+
|
|
368
|
+
creds = Credentials.fetch(ds_key)
|
|
369
|
+
next if creds.empty?
|
|
370
|
+
|
|
371
|
+
merged_datasources[ds_key] = config.merge(creds)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
{datasources_path => YAML.dump(merged_datasources)}
|
|
375
|
+
end
|
|
376
|
+
|
|
354
377
|
def submit_deployment(config, branch_id, archive_path, metadata)
|
|
355
378
|
client = API::Client.new(config["server"], config["api_key"])
|
|
356
379
|
|
|
@@ -16,7 +16,7 @@ module Strata
|
|
|
16
16
|
module Archive
|
|
17
17
|
module_function
|
|
18
18
|
|
|
19
|
-
def create(project_path, files_to_include: nil)
|
|
19
|
+
def create(project_path, files_to_include: nil, file_overrides: {})
|
|
20
20
|
if files_to_include
|
|
21
21
|
# Use provided file list, but ensure they exist and are yml files
|
|
22
22
|
files = files_to_include.select do |file_path|
|
|
@@ -30,7 +30,7 @@ module Strata
|
|
|
30
30
|
end
|
|
31
31
|
files = exclude_secrets(files)
|
|
32
32
|
inlined_files = process_imports(files, project_path)
|
|
33
|
-
build_archive(files, project_path, inlined_files)
|
|
33
|
+
build_archive(files, project_path, inlined_files, file_overrides)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
private_class_method def collect_yml_files(project_path)
|
|
@@ -87,14 +87,14 @@ module Strata
|
|
|
87
87
|
inlined_files
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
def build_archive(files, project_path, inlined_files = {})
|
|
90
|
+
def build_archive(files, project_path, inlined_files = {}, file_overrides = {})
|
|
91
91
|
archive_file = archive_path
|
|
92
92
|
|
|
93
93
|
File.open(archive_file, "wb") do |file|
|
|
94
94
|
Zlib::GzipWriter.open(file) do |gz|
|
|
95
95
|
Gem::Package::TarWriter.new(gz) do |tar|
|
|
96
96
|
files.each do |file_path|
|
|
97
|
-
add_file_to_tar(tar, file_path, project_path, inlined_files)
|
|
97
|
+
add_file_to_tar(tar, file_path, project_path, inlined_files, file_overrides)
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
end
|
|
@@ -103,10 +103,19 @@ module Strata
|
|
|
103
103
|
archive_file
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
private_class_method def add_file_to_tar(tar, file_path, project_path, inlined_files = {})
|
|
106
|
+
private_class_method def add_file_to_tar(tar, file_path, project_path, inlined_files = {}, file_overrides = {})
|
|
107
107
|
relative_path = file_path.sub("#{project_path}/", "")
|
|
108
|
+
override_content = file_overrides[file_path]
|
|
108
109
|
|
|
109
|
-
if
|
|
110
|
+
if override_content
|
|
111
|
+
content = override_content.is_a?(String) ? override_content : YAML.dump(override_content)
|
|
112
|
+
size = content.bytesize
|
|
113
|
+
mode = 0o644
|
|
114
|
+
|
|
115
|
+
tar.add_file_simple(relative_path, mode, size) do |tar_file|
|
|
116
|
+
tar_file.write(content)
|
|
117
|
+
end
|
|
118
|
+
elsif inlined_files[file_path]
|
|
110
119
|
content = YAML.dump(inlined_files[file_path])
|
|
111
120
|
size = content.bytesize
|
|
112
121
|
mode = 0o644
|
data/lib/strata/cli/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: strata-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ajo Abraham
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.
|
|
19
|
+
version: 0.3.0
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.
|
|
26
|
+
version: 0.3.0
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: aws-sdk-athena
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -268,7 +268,9 @@ files:
|
|
|
268
268
|
- lib/strata/cli/generators/project.rb
|
|
269
269
|
- lib/strata/cli/generators/relation.rb
|
|
270
270
|
- lib/strata/cli/generators/table.rb
|
|
271
|
+
- lib/strata/cli/generators/templates/AGENTS.md
|
|
271
272
|
- lib/strata/cli/generators/templates/adapters/athena.yml
|
|
273
|
+
- lib/strata/cli/generators/templates/adapters/databricks.yml
|
|
272
274
|
- lib/strata/cli/generators/templates/adapters/druid.yml
|
|
273
275
|
- lib/strata/cli/generators/templates/adapters/duckdb.yml
|
|
274
276
|
- lib/strata/cli/generators/templates/adapters/mysql.yml
|