strata-cli 0.1.0.beta

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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.standard.yml +3 -0
  3. data/CHANGELOG.md +5 -0
  4. data/CLAUDE.md +65 -0
  5. data/LICENSE +21 -0
  6. data/README.md +465 -0
  7. data/Rakefile +10 -0
  8. data/exe/strata +6 -0
  9. data/lib/strata/cli/ai/client.rb +63 -0
  10. data/lib/strata/cli/ai/configuration.rb +48 -0
  11. data/lib/strata/cli/ai/services/table_generator.rb +282 -0
  12. data/lib/strata/cli/api/client.rb +170 -0
  13. data/lib/strata/cli/api/connection_error_handler.rb +54 -0
  14. data/lib/strata/cli/configuration.rb +135 -0
  15. data/lib/strata/cli/credentials.rb +83 -0
  16. data/lib/strata/cli/descriptions/create/migration.txt +25 -0
  17. data/lib/strata/cli/descriptions/create/relation.txt +14 -0
  18. data/lib/strata/cli/descriptions/create/table.txt +23 -0
  19. data/lib/strata/cli/descriptions/datasource/add.txt +15 -0
  20. data/lib/strata/cli/descriptions/datasource/auth.txt +14 -0
  21. data/lib/strata/cli/descriptions/datasource/exec.txt +7 -0
  22. data/lib/strata/cli/descriptions/datasource/meta.txt +11 -0
  23. data/lib/strata/cli/descriptions/datasource/tables.txt +12 -0
  24. data/lib/strata/cli/descriptions/datasource/test.txt +8 -0
  25. data/lib/strata/cli/descriptions/deploy/deploy.txt +24 -0
  26. data/lib/strata/cli/descriptions/deploy/status.txt +9 -0
  27. data/lib/strata/cli/descriptions/init.txt +14 -0
  28. data/lib/strata/cli/generators/datasource.rb +83 -0
  29. data/lib/strata/cli/generators/group.rb +13 -0
  30. data/lib/strata/cli/generators/migration.rb +71 -0
  31. data/lib/strata/cli/generators/project.rb +190 -0
  32. data/lib/strata/cli/generators/relation.rb +64 -0
  33. data/lib/strata/cli/generators/table.rb +143 -0
  34. data/lib/strata/cli/generators/templates/adapters/athena.yml +53 -0
  35. data/lib/strata/cli/generators/templates/adapters/druid.yml +42 -0
  36. data/lib/strata/cli/generators/templates/adapters/duckdb.yml +36 -0
  37. data/lib/strata/cli/generators/templates/adapters/mysql.yml +45 -0
  38. data/lib/strata/cli/generators/templates/adapters/postgres.yml +48 -0
  39. data/lib/strata/cli/generators/templates/adapters/snowflake.yml +69 -0
  40. data/lib/strata/cli/generators/templates/adapters/sqlserver.yml +45 -0
  41. data/lib/strata/cli/generators/templates/adapters/trino.yml +56 -0
  42. data/lib/strata/cli/generators/templates/datasources.yml +4 -0
  43. data/lib/strata/cli/generators/templates/migration.rename.yml +15 -0
  44. data/lib/strata/cli/generators/templates/migration.swap.yml +13 -0
  45. data/lib/strata/cli/generators/templates/project.yml +36 -0
  46. data/lib/strata/cli/generators/templates/rel.domain.yml +43 -0
  47. data/lib/strata/cli/generators/templates/strata.yml +24 -0
  48. data/lib/strata/cli/generators/templates/table.table_name.yml +118 -0
  49. data/lib/strata/cli/generators/templates/test.yml +34 -0
  50. data/lib/strata/cli/generators/test.rb +48 -0
  51. data/lib/strata/cli/guard.rb +21 -0
  52. data/lib/strata/cli/helpers/color_helper.rb +103 -0
  53. data/lib/strata/cli/helpers/command_context.rb +41 -0
  54. data/lib/strata/cli/helpers/datasource_helper.rb +62 -0
  55. data/lib/strata/cli/helpers/description_helper.rb +18 -0
  56. data/lib/strata/cli/helpers/project_helper.rb +85 -0
  57. data/lib/strata/cli/helpers/prompts.rb +42 -0
  58. data/lib/strata/cli/helpers/table_filter.rb +48 -0
  59. data/lib/strata/cli/main.rb +71 -0
  60. data/lib/strata/cli/sub_commands/audit.rb +262 -0
  61. data/lib/strata/cli/sub_commands/create.rb +419 -0
  62. data/lib/strata/cli/sub_commands/datasource.rb +353 -0
  63. data/lib/strata/cli/sub_commands/deploy.rb +433 -0
  64. data/lib/strata/cli/sub_commands/project.rb +38 -0
  65. data/lib/strata/cli/sub_commands/table.rb +58 -0
  66. data/lib/strata/cli/terminal.rb +102 -0
  67. data/lib/strata/cli/ui/autocomplete.rb +93 -0
  68. data/lib/strata/cli/ui/field_editor.rb +215 -0
  69. data/lib/strata/cli/utils/archive.rb +137 -0
  70. data/lib/strata/cli/utils/deployment_monitor.rb +445 -0
  71. data/lib/strata/cli/utils/git.rb +253 -0
  72. data/lib/strata/cli/utils/import_manager.rb +190 -0
  73. data/lib/strata/cli/utils/test_reporter.rb +131 -0
  74. data/lib/strata/cli/utils/yaml_import_resolver.rb +91 -0
  75. data/lib/strata/cli/utils.rb +39 -0
  76. data/lib/strata/cli/version.rb +7 -0
  77. data/lib/strata/cli.rb +36 -0
  78. data/sig/strata/cli.rbs +6 -0
  79. metadata +306 -0
@@ -0,0 +1,43 @@
1
+ # Define relationshps between tables for a given universe
2
+
3
+ # Required: The datasource these relationships are valid for.
4
+ # The chosen tables will be scoped to those within this datasource.
5
+ # Strata does not support cross datasource joins
6
+ datasource: "<datasource_name>"
7
+
8
+ # Define relationships between tables. The table names used here should
9
+ # correspond to the name given in its respective model file and not the
10
+ # physical name (often they are same).
11
+ # NOTE: Strata does not support many_to_many joins.
12
+
13
+ # Example: One-to-many relationship (customer has many orders)
14
+ # customer_orders:
15
+ # left: "customers"
16
+ # right: "orders"
17
+ # join: "left.id = right.customer_id"
18
+ # cardinality: "one_to_many"
19
+
20
+ # Example: Many-to-one relationship (orders belong to customer)
21
+ # order_customer:
22
+ # left: "orders"
23
+ # right: "customers"
24
+ # join: "left.customer_id = right.id"
25
+ # cardinality: "many_to_one"
26
+ # # Whether measurs should be aggregated from the low cardinality
27
+ # # table. In most cases this will overcount.
28
+ # allow_measure_expansion: true|false (default false)
29
+
30
+ # Example: One-to-one relationship (user has one profile)
31
+ # user_profile:
32
+ # left: "users"
33
+ # right: "user_profiles"
34
+ # join: "left.id = right.user_id"
35
+ # cardinality: "one_to_one"
36
+
37
+ # Example: Compound join
38
+ # user_roles:
39
+ # left: "users"
40
+ # right: "roles"
41
+ # join: "left.id = right.user_id AND left.id = right.role_id"
42
+ # cardinality: "one_to_many"
43
+
@@ -0,0 +1,24 @@
1
+ # Strata Configuration file. Do not check this into git.
2
+ #
3
+ # This file contains credentials and local-only configuration.
4
+ # Project-level configuration (project_id, server) should be in project.yml.
5
+ #
6
+ # Strata uses API keys to configure access. Please get your API key
7
+ # from your Strata admins. You can regenerate/revoke API keys as
8
+ # needed on the server. You must be Developer on a Project to modify
9
+ # it or you must be System Manager to be able to create new projects.
10
+ api_key: <%= options[:api_key] || "YOUR_STRATA_API_KEY" %>
11
+
12
+ # Alternatively, you can configure API keys per environment (different Strata server instances).
13
+ # Each environment represents a separate Strata cluster (dev, staging, production).
14
+ # Use -e [ENVIRONMENT] flag when deploying to select the environment.
15
+ # Example:
16
+ # production:
17
+ # api_key: YOUR_PRODUCTION_API_KEY
18
+ # staging:
19
+ # api_key: YOUR_STAGING_API_KEY
20
+ #
21
+
22
+ # Credentials here are stored per datasource. These credentials
23
+ # should be set via `strata ds auth DS_KEY`. DS_KEY should match the datasouces
24
+ # yaml node key for the datasource you are trying to update.
@@ -0,0 +1,118 @@
1
+ # Semantic Table Model
2
+ #
3
+ # Define the Measures and Dimensions that should be attached to this table.
4
+ # Note: Relationships are managed separately and should not be defined in this file.
5
+
6
+ # Required: The datasource this table belongs to. This should be
7
+ # either the datasource key or datasource name.
8
+ datasource: "<datasource_name>"
9
+
10
+ # Required: The logical name for this table in the semantic model. Must be unique
11
+ # within this datasource. You can have mulitple tables point to the same physical table.
12
+ name: "<table_name>"
13
+
14
+ # Required: The physical table name in the actual database. This can be prefixed
15
+ # with catalog and schema as needed. Be sure to check whether your data wareshouse
16
+ # supports cross schema/catalog queries.
17
+ physical_name: "<physical_table_name>"
18
+
19
+ # Required: Cost number influences how the table is preferentially
20
+ # selected. If multiple tables can answer the same question,
21
+ # the lowest cost table is selected..
22
+ # Dimension tables should generally be lower cost and your
23
+ # hot tier tables should be lower than cold tier.
24
+ cost: 10
25
+
26
+ # Optional: If this is a snapshot table like an inventory table, you can
27
+ # set the snapshot date dimension here. This will allow the creation of
28
+ # snapshot measures on this table.
29
+ # snapshot_date: <snapshot dimension name>
30
+
31
+ # Optional: Tag table for additional metadata.
32
+ # tags:
33
+ # - marketing
34
+ # - ops
35
+
36
+ # Optional: Define table partitioning constraints
37
+ # This tells the semantic engine about data availability/constraints
38
+ # Only **between** and **in_list** predicates are supported.
39
+ # partition:
40
+ # # Example: Table only contains 2 years of data
41
+ # # *Must reference a dimension mapped to this table.
42
+ # - dimension: Region Date
43
+ # predicate: between
44
+ # filter_value: 2y
45
+ # filter_value_end: 1d
46
+ # description: "Table only has rolling 2 year data"
47
+ #
48
+ # # Example: Table is partitioned by region
49
+ # - dimension: Region
50
+ # predicate: in_list
51
+ # filter_value: us-east, us-west, europe
52
+ # description: "Table only has 3 regions of 5"
53
+
54
+ # Imports are processed first, then local field definitions are merged in
55
+ # This allows you to inherit common field definitions and override or extend them
56
+ # Paths should be relative to this file.
57
+ # imports:
58
+ # - "path/to/other/table.yml"
59
+ # - "common/shared_fields.yml"
60
+
61
+ # Define Measures and dimensions that should be mapped to this table
62
+ fields:
63
+ # Example field definition structure:
64
+ # - type: dimension|measure (Required)
65
+ # name: My Field Name (Required)
66
+ # description: Describing my awesome field
67
+ # data_type: string|integer|bigint|decimal|date|date_time|boolean|binary (Required)
68
+ # hidden: true|false
69
+ #
70
+ # # Optional: UI rendering of the field
71
+ # display_type: default|html|url|email|phone_number|image
72
+ #
73
+ # # Optional: Specific formatting to be applied (will supercede display_type)
74
+ # formatter: currency_usd|percent|thousands|millions|billions|(custom js function using numeraljs/momentjs)
75
+ #
76
+ # # Optional: disable listing individual elements (dimension only). Good to do that for
77
+ # # high cardinality columns like account_id
78
+ # disable_value_listing: true|false
79
+ # # Optional: limit on items from this field that should be shown. Will affect list cache size.
80
+ # # Dimensions only. Default is 1000
81
+ # value_list_size: 1000
82
+ #
83
+ # # Optional: For date/time types only. Set of grainularities this date should support.
84
+ # # Default all. raw, second, minute, hour, day, week, month, quarter, year
85
+ # grains:
86
+ # - day
87
+ # - week
88
+ # - month
89
+ # - quarter
90
+ # - year
91
+ #
92
+ # # Required: Defines how this field will query this table
93
+ # expresssion:
94
+ # primary_key: true|false (optional)
95
+ # lookup: true|false (optional)
96
+ # array: true|false (optional)
97
+ # sql: my_field_column (Required)
98
+ #
99
+ # # Optional: Exclude certain dimnesions from the group by/filter
100
+ # exclusion_type: exclude|exclude_all_except|exclude_all
101
+ # exclusions: # (Required when exclusion type is set and isnt exclude_all)
102
+ # - type: table|dimension|universe (Required)
103
+ # filter: ignore|only|apply (Required)
104
+ # entities:
105
+ # - <entity name>
106
+ # - <entity name>
107
+ #
108
+ # # Optional: Inclusions force calculation at lower level before rolling up
109
+ # inclusions:
110
+ # filter: ignore|only|apply
111
+ # aggregation: percentile_cont(0.5) WITHIN GROUP (ORDER BY @exp) # Final rollup calculation. Base calc is expression -> sql
112
+ # dimensions:
113
+ # - <dimension name>
114
+ # - <dimension name>
115
+ #
116
+ # # Optional: Snapshot measures type. Will force calc to starting or ending period.
117
+ # # Table must also set Snapshot date value.
118
+ # snapshot: ending|beginning
@@ -0,0 +1,34 @@
1
+ # Test Definition
2
+ #
3
+ # This file defines a test case for validating semantic model queries.
4
+ # Tests verify that the Planner generates correct SQL from your semantic model.
5
+ # Tests run on the server during deployment.
6
+
7
+ # Required: Display name for this test
8
+ name: <test_name>
9
+
10
+ # Required: List of semantic field names to query
11
+ # These must match field names defined in your model files
12
+ projections:
13
+ - Field Name 1
14
+ - Field Name 2
15
+
16
+ # Optional: Filter conditions to apply to the query
17
+ # filters:
18
+ # - field: Date Field
19
+ # predicate: between
20
+ # filter_value: "2024-01-01"
21
+ # filter_value_end: "2024-12-31"
22
+ # - field: Status
23
+ # predicate: equals
24
+ # filter_value: "active"
25
+
26
+ # Required: At least ONE assertion must be present (assert_sql or assert_regex)
27
+
28
+ # Full SQL assertion - the complete SQL you expect the Planner to generate
29
+ # Use this for exact matching (whitespace normalized)
30
+ # assert_sql: "SELECT column1, column2 FROM table_name WHERE condition"
31
+
32
+ # OR Regex assertion - pattern to match against generated SQL
33
+ # Use this for partial/flexible matching
34
+ # assert_regex: "column_name.*table_name"
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "group"
4
+ require "yaml"
5
+
6
+ module Strata
7
+ module CLI
8
+ module Generators
9
+ # Generates test YAML files from templates.
10
+ class Test < Group
11
+ argument :name, type: :string, desc: "Test name"
12
+
13
+ def create_test_file
14
+ output_path = File.join("tests", "#{slugify(name)}.yml")
15
+
16
+ # Ensure directory exists
17
+ empty_directory "tests"
18
+
19
+ # Load template and update with user inputs
20
+ template_content = load_template
21
+ updated_content = update_template(template_content)
22
+
23
+ # Write the updated template
24
+ create_file output_path, updated_content
25
+
26
+ say_status :created, output_path, :green
27
+ end
28
+
29
+ private
30
+
31
+ def load_template
32
+ template_path = File.join(File.dirname(__FILE__), "templates", "test.yml")
33
+ File.read(template_path)
34
+ end
35
+
36
+ def update_template(template_content)
37
+ template_content.gsub("<test_name>", name)
38
+ end
39
+
40
+ def slugify(text)
41
+ text.to_s.downcase
42
+ .gsub(/[^a-z0-9]+/, "_")
43
+ .gsub(/^_|_$/, "")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ require_relative "utils"
2
+ module Strata
3
+ module CLI
4
+ module Guard
5
+ ALLOWED_COMMANDS = %w[
6
+ init
7
+ help
8
+ adapters
9
+ version
10
+ deploy
11
+ ]
12
+ def invoke_command(command, *args)
13
+ Utils.exit_error_if_not_strata! unless ALLOWED_COMMANDS.include?(command.name)
14
+ super
15
+ rescue Strata::CommandError => e
16
+ shell.say_error "ERROR: #{e.message}", :red
17
+ exit 1
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module Strata
6
+ module CLI
7
+ module ColorHelper
8
+ THEME = {
9
+ success: :green,
10
+ error: %i[red bold],
11
+ warning: :yellow,
12
+ info: :cyan,
13
+ title: %i[cyan bold],
14
+ highlight: %i[cyan bold],
15
+ dim: :bright_black,
16
+ primary: :blue,
17
+ secondary: :magenta,
18
+ border: %i[cyan dim],
19
+ selected: %i[green bold],
20
+ disabled: :dim
21
+ }.freeze
22
+
23
+ class << self
24
+ def pastel
25
+ @pastel ||= Pastel.new(enabled: $stdout.tty?)
26
+ end
27
+
28
+ def success(text = nil)
29
+ apply_theme(:success, text)
30
+ end
31
+
32
+ def error(text = nil)
33
+ apply_theme(:error, text)
34
+ end
35
+
36
+ def warning(text = nil)
37
+ apply_theme(:warning, text)
38
+ end
39
+
40
+ def info(text = nil)
41
+ apply_theme(:info, text)
42
+ end
43
+
44
+ def title(text = nil)
45
+ apply_theme(:title, text)
46
+ end
47
+
48
+ def highlight(text = nil)
49
+ apply_theme(:highlight, text)
50
+ end
51
+
52
+ def dim(text = nil)
53
+ apply_theme(:dim, text)
54
+ end
55
+
56
+ def primary(text = nil)
57
+ apply_theme(:primary, text)
58
+ end
59
+
60
+ def secondary(text = nil)
61
+ apply_theme(:secondary, text)
62
+ end
63
+
64
+ def border(text = nil)
65
+ apply_theme(:border, text)
66
+ end
67
+
68
+ def selected(text = nil)
69
+ apply_theme(:selected, text)
70
+ end
71
+
72
+ def disabled(text = nil)
73
+ apply_theme(:disabled, text)
74
+ end
75
+
76
+ def bright_cyan(text = nil)
77
+ text ? pastel.bright_cyan(text) : :bright_cyan
78
+ end
79
+
80
+ def bright_green(text = nil)
81
+ text ? pastel.bright_green(text) : :bright_green
82
+ end
83
+
84
+ def bright_yellow(text = nil)
85
+ text ? pastel.bright_yellow(text) : :bright_yellow
86
+ end
87
+
88
+ private
89
+
90
+ def apply_theme(key, text)
91
+ styles = THEME[key]
92
+ return styles unless text
93
+
94
+ if styles.is_a?(Array)
95
+ pastel.decorate(text, *styles)
96
+ else
97
+ pastel.send(styles, text)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require_relative "datasource_helper"
5
+ require_relative "color_helper"
6
+ require_relative "prompts"
7
+
8
+ module Strata
9
+ module CLI
10
+ module Helpers
11
+ module CommandContext
12
+ include DatasourceHelper
13
+
14
+ def adapter
15
+ @adapter ||= create_adapter(datasource_key)
16
+ end
17
+
18
+ def prompt
19
+ @prompt ||= TTY::Prompt.new
20
+ end
21
+
22
+ def datasource_key
23
+ @datasource_key ||= resolve_datasource(prompt: prompt)
24
+ end
25
+
26
+ def all_tables
27
+ @all_tables ||= begin
28
+ tables = with_spinner("Fetching tables from #{datasource_key}...") { adapter.tables }
29
+ if tables.empty?
30
+ # Assuming Prompts and ColorHelper are available in the class or via module
31
+ say Prompts::MSG_NO_TABLES_FOUND % datasource_key, ColorHelper.warning
32
+ []
33
+ else
34
+ tables
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strata
4
+ module CLI
5
+ module DatasourceHelper
6
+ def resolve_datasource(ds_key_arg = nil, prompt: TTY::Prompt.new)
7
+ # 1. Use argument if provided
8
+ return validate_datasource(ds_key_arg) if ds_key_arg
9
+
10
+ # 2. Use option if provided
11
+ return validate_datasource(options[:datasource]) if options[:datasource]
12
+
13
+ # 3. Check available datasources
14
+ ds_keys = datasources.keys
15
+
16
+ if ds_keys.empty?
17
+ say "No datasources configured. Run 'strata datasource add' first.", :red
18
+ nil
19
+ elsif ds_keys.length == 1
20
+ # Auto-select if only one
21
+ ds_key = ds_keys.first
22
+ say "Using datasource: #{ds_key}", :cyan
23
+ ds_key
24
+ else
25
+ # Prompt selection if multiple
26
+ prompt.select("Select datasource:", ds_keys)
27
+ end
28
+ end
29
+
30
+ def create_adapter(ds_key)
31
+ config = ds_config(ds_key).merge(Credentials.fetch(ds_key))
32
+ DWH.create(config["adapter"].to_sym, config)
33
+ end
34
+
35
+ def ds_config(ds_key)
36
+ unless datasources.key?(ds_key)
37
+ raise "Datasource definition with key #{ds_key} was not found in datasources.yml file."
38
+ end
39
+
40
+ datasources[ds_key]
41
+ end
42
+
43
+ private
44
+
45
+ def validate_datasource(ds_key)
46
+ unless datasources[ds_key]
47
+ say "Error: Datasource '#{ds_key}' not found in datasources.yml", :red
48
+ return nil
49
+ end
50
+ ds_key
51
+ end
52
+
53
+ def datasources
54
+ @datasources_cache ||= begin
55
+ YAML.safe_load_file("datasources.yml", permitted_classes: [Date, Time], aliases: true) || {}
56
+ rescue Errno::ENOENT
57
+ {}
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,18 @@
1
+ module Strata
2
+ module CLI
3
+ module Helpers
4
+ module DescriptionHelper
5
+ def long_desc_from_file(path)
6
+ # Descriptions are in lib/strata/cli/descriptions/, not in helpers/
7
+ descriptions_dir = File.join(File.dirname(__dir__), "descriptions")
8
+ file_path = File.join(descriptions_dir, "#{path}.txt")
9
+ if File.exist?(file_path)
10
+ long_desc File.read(file_path)
11
+ else
12
+ warn "Warning: Description file not found at #{file_path}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "../utils/git"
5
+
6
+ module Strata
7
+ module CLI
8
+ module Helpers
9
+ module ProjectHelper
10
+ module_function
11
+
12
+ # Persists project_id to project.yml file
13
+ def persist_project_id_to_yml(project_id, project_yml_path: "project.yml")
14
+ return false unless project_id && !project_id.to_s.strip.empty?
15
+ return false unless File.exist?(project_yml_path)
16
+
17
+ project_config = YAML.safe_load_file(project_yml_path, permitted_classes: [Date, Time], aliases: true) || {}
18
+ return false if project_config["project_id"] && !project_config["project_id"].to_s.strip.empty?
19
+
20
+ project_yml_content = File.read(project_yml_path)
21
+
22
+ begin
23
+ if /^#\s*project_id:\s*$/.match?(project_yml_content)
24
+ updated_content = project_yml_content.gsub(/^#\s*project_id:\s*$/, "project_id: #{project_id}")
25
+ File.write(project_yml_path, updated_content)
26
+ else
27
+ warning_comment = <<~YAML
28
+ # WARNING: Do not change this project_id. It links this project to the Strata server.
29
+ # Changing it may result in creating a new project or deployment failures.
30
+ project_id: #{project_id}
31
+ YAML
32
+
33
+ File.open(project_yml_path, "a") do |f|
34
+ f.puts "\n" unless project_yml_content.end_with?("\n")
35
+ f.puts warning_comment
36
+ end
37
+ end
38
+
39
+ true
40
+ rescue Errno::EACCES => e
41
+ raise Strata::CommandError, "Permission denied writing to #{project_yml_path}: #{e.message}"
42
+ rescue Errno::ENOSPC => e
43
+ raise Strata::CommandError, "Disk full: #{e.message}"
44
+ rescue => e
45
+ raise Strata::CommandError, "Failed to write to #{project_yml_path}: #{e.message}"
46
+ end
47
+ end
48
+
49
+ # Persists git URL to project.yml file if missing
50
+ def persist_git_url_if_missing(project_yml_path: "project.yml")
51
+ return false unless File.exist?(project_yml_path)
52
+
53
+ project_config = YAML.safe_load_file(project_yml_path, permitted_classes: [Date, Time], aliases: true) || {}
54
+ return false if project_config["git"] && !project_config["git"].to_s.strip.empty?
55
+
56
+ git_remote = Utils::Git.git_remote_url
57
+ return false unless git_remote
58
+
59
+ normalized_url = git_remote.chomp("/").chomp(".git")
60
+ project_yml_content = File.read(project_yml_path)
61
+
62
+ begin
63
+ if /^(\s*)git:\s*$/.match?(project_yml_content)
64
+ updated_content = project_yml_content.gsub(/^(\s*)git:\s*$/, "\\1git: #{normalized_url}")
65
+ File.write(project_yml_path, updated_content)
66
+ else
67
+ File.open(project_yml_path, "a") do |f|
68
+ f.puts "\n" unless project_yml_content.end_with?("\n")
69
+ f.puts "git: #{normalized_url}"
70
+ end
71
+ end
72
+
73
+ true
74
+ rescue Errno::EACCES => e
75
+ raise Strata::CommandError, "Permission denied writing to #{project_yml_path}: #{e.message}"
76
+ rescue Errno::ENOSPC => e
77
+ raise Strata::CommandError, "Disk full: #{e.message}"
78
+ rescue => e
79
+ raise Strata::CommandError, "Failed to write to #{project_yml_path}: #{e.message}"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,42 @@
1
+ module Strata
2
+ module CLI
3
+ module Prompts
4
+ # Relation Command Prompts
5
+ MSG_SELECT_LEFT_TABLE = "Select LEFT table (the 'many' side usually):"
6
+ MSG_SELECT_RIGHT_TABLE_ALL = "Select RIGHT table (All):"
7
+ MSG_SELECT_RIGHT_TABLE_SUGGESTED = "Select RIGHT table (Suggested):"
8
+ MSG_NO_TABLES_FOUND = "No tables found in %s"
9
+ MSG_SHOW_OTHER_TABLES = "Show other tables..."
10
+ MSG_SELECT_FROM_LIST = "Select from list instead?"
11
+ MSG_JOIN_CONDITION = " Join Condition (SQL):"
12
+ MSG_RELATION_PATH = " Relationship Path:"
13
+
14
+ # Table Command Prompts
15
+ MSG_SEARCH_TABLE = "Search table:"
16
+ MSG_MODEL_DISPLAY_NAME = " Model display name:"
17
+ MSG_MODEL_DESCRIPTION = " Model description (optional):"
18
+ MSG_NO_FIELDS_CONFIRMED = "\n⚠️ Warning: No fields confirmed. All fields may have been skipped."
19
+ MSG_CONTINUE_NO_FIELDS = "Continue creating model with no fields?"
20
+ MSG_FIELDS_CONFIRMED = "\n %d field(s) confirmed"
21
+ MSG_CREATED_MODEL = "\n✔ Created %s"
22
+ MSG_EDIT_MODEL_HINT = "\n💡 Edit the file to customize field names & expressions"
23
+
24
+ MSG_NO_MODELS_DIR = "No models directory found. Run 'strata create table' to create your first model."
25
+ MSG_NO_MODELS_FOUND = "No models found. Run 'strata create table' to create one."
26
+ MSG_MODELS_LIST_HEADER = "\n Semantic Models:\n"
27
+ MSG_MODELS_COUNT = "\n Total: %d model(s)\n"
28
+
29
+ # Migration hook options
30
+ MIGRATION_HOOK_OPTIONS = {
31
+ "pre (before deployment)" => "pre",
32
+ "post (after deployment)" => "post"
33
+ }.freeze
34
+
35
+ module_function
36
+
37
+ def default_migration_hook(operation)
38
+ (operation == "swap") ? "post (after deployment)" : "pre (before deployment)"
39
+ end
40
+ end
41
+ end
42
+ end