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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91fc67146618378d16d1431f8b85cb76f470554fb0488cbb022c4328a34da844
4
- data.tar.gz: 63e2fcb80f04e9b90a56b76b2c6d0ea1be533affbe8404f23cc646dcf0fa7c95
3
+ metadata.gz: e87ebe6383ea4dea3c2b0a57d47bc8a70cdbad507bcb9233614fb33161f5e71a
4
+ data.tar.gz: c343df7299ff6959a73e31e68ca3cc959c5e9596dceb671be4d25868c9c04a5e
5
5
  SHA512:
6
- metadata.gz: 459379ea3a5868ec9a1fd7380040609e530e6bcf35aa78ded012f8664f4da7d9aa491d2e88549de031b9d384a5c7b4b50788b1d0a109dad6ebce4658479a8652
7
- data.tar.gz: b3cfb8ffee0d9e6db726ecb9e3849dfd5c685956d55d0ff66317bb2b3e7a514f3b11e7e802014c40037dc4819d67a4c32981b83830181151ac5eae5cd90d7634
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, OAuth)
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.0-flash"
29
- when "openai" then "gpt-4o-mini"
30
- when "anthropic" then "claude-sonnet-4-20250514"
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 analytics and BI systems.
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
- For each column, determine:
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 2-4 alternative names users might use to refer to this field.
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: "Customer ID" → ["cust id", "client id", "buyer id"]
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/tbl_*.yml").first(5) # Limit to 5 for performance
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.map do |file|
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["schema_type"]})"
173
- end.join(", ")
174
- "Model: #{model["name"]} - Fields: #{fields_summary}"
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.compact
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 oauth], default: "pat")
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)
@@ -78,6 +78,12 @@ module Strata
78
78
  persist_project_id_to_project_yml
79
79
  end
80
80
 
81
+ def create_agents_file
82
+ return if cloned_from_git?
83
+
84
+ copy_file "AGENTS.md", "#{uid}/AGENTS.md"
85
+ end
86
+
81
87
  def create_datasources_file
82
88
  return if cloned_from_git?
83
89
 
@@ -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, oauth
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 joins
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 joins.
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
- # join: "left.id = right.customer_id"
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
- # join: "left.customer_id = right.id"
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
- # join: "left.id = right.user_id"
34
+ # sql: "left.id = right.user_id"
35
35
  # cardinality: "one_to_one"
36
36
 
37
- # Example: Compound join
37
+ # Example: Compound Join
38
38
  # user_roles:
39
39
  # left: "users"
40
40
  # right: "roles"
41
- # join: "left.id = right.user_id AND left.id = right.role_id"
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
- prompt.select(" Authentication mode:", %w[pat kp oauth], default: default_value)
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.sub(%r{/\z}, "") # normalize: remove trailing slash
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 inlined_files[file_path]
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Strata
4
4
  module CLI
5
- VERSION = "0.1.6.beta"
5
+ VERSION = "0.1.8"
6
6
  end
7
7
  end
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.6.beta
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.1.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.1.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