strata-cli 0.1.15 → 0.1.16
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 +20 -0
- data/lib/strata/cli/agent_mode.rb +23 -1
- data/lib/strata/cli/descriptions/init.txt +4 -0
- data/lib/strata/cli/generators/project.rb +20 -4
- data/lib/strata/cli/generators/templates/AGENTS.md +4 -0
- data/lib/strata/cli/helpers/browser_auth.rb +52 -0
- data/lib/strata/cli/helpers/command_context.rb +1 -5
- data/lib/strata/cli/helpers/datasource_helper.rb +1 -1
- data/lib/strata/cli/helpers/prompts.rb +27 -0
- data/lib/strata/cli/helpers/semantic_audit_checks.rb +507 -0
- data/lib/strata/cli/sub_commands/audit.rb +24 -6
- data/lib/strata/cli/sub_commands/branch.rb +0 -1
- data/lib/strata/cli/sub_commands/datasource.rb +7 -4
- data/lib/strata/cli/sub_commands/deploy.rb +42 -22
- data/lib/strata/cli/sub_commands/project.rb +2 -42
- data/lib/strata/cli/utils/git.rb +31 -9
- data/lib/strata/cli/utils/sql_expression_parser.rb +153 -0
- data/lib/strata/cli/version.rb +1 -1
- metadata +5 -2
|
@@ -5,6 +5,7 @@ require_relative "../terminal"
|
|
|
5
5
|
require_relative "../output"
|
|
6
6
|
require_relative "../helpers/project_helper"
|
|
7
7
|
require_relative "../helpers/description_helper"
|
|
8
|
+
require_relative "../helpers/browser_auth"
|
|
8
9
|
require_relative "../agent_mode"
|
|
9
10
|
require_relative "../api/client"
|
|
10
11
|
require_relative "../credentials"
|
|
@@ -25,6 +26,7 @@ module Strata
|
|
|
25
26
|
include Terminal
|
|
26
27
|
include Output
|
|
27
28
|
include AgentMode
|
|
29
|
+
include Helpers::BrowserAuth
|
|
28
30
|
extend Helpers::DescriptionHelper
|
|
29
31
|
|
|
30
32
|
default_command :deploy
|
|
@@ -37,6 +39,9 @@ module Strata
|
|
|
37
39
|
desc "deploy", "Deploy project to Strata server for the current branch"
|
|
38
40
|
long_desc_from_file "deploy/deploy"
|
|
39
41
|
def deploy
|
|
42
|
+
# Before anything that prompts, signs in, or hits the server.
|
|
43
|
+
ensure_working_tree_clean
|
|
44
|
+
|
|
40
45
|
config = load_deployment_configuration
|
|
41
46
|
validate_deployment_configuration(config)
|
|
42
47
|
run_pre_deploy_checks unless options[:skip_audit]
|
|
@@ -128,8 +133,9 @@ module Strata
|
|
|
128
133
|
end
|
|
129
134
|
|
|
130
135
|
def validate_deployment_configuration(config)
|
|
131
|
-
|
|
136
|
+
# Server first: browser sign-in needs its URL to build the auth link
|
|
132
137
|
ensure_server(config)
|
|
138
|
+
ensure_api_key(config)
|
|
133
139
|
ensure_git_url_populated
|
|
134
140
|
ensure_project_id(config)
|
|
135
141
|
end
|
|
@@ -139,7 +145,7 @@ module Strata
|
|
|
139
145
|
return
|
|
140
146
|
end
|
|
141
147
|
|
|
142
|
-
config["api_key"] = collect_api_key_interactively
|
|
148
|
+
config["api_key"] = collect_api_key_interactively(config["server"])
|
|
143
149
|
end
|
|
144
150
|
|
|
145
151
|
def ensure_server(config)
|
|
@@ -149,8 +155,6 @@ module Strata
|
|
|
149
155
|
end
|
|
150
156
|
|
|
151
157
|
def collect_server_interactively
|
|
152
|
-
prompt = TTY::Prompt.new
|
|
153
|
-
|
|
154
158
|
say "\nServer URL not found (not in project.yml)", :yellow
|
|
155
159
|
say "You can add it to project.yml or enter it now here\n", :cyan
|
|
156
160
|
|
|
@@ -199,16 +203,24 @@ module Strata
|
|
|
199
203
|
persist_project_id(project_id)
|
|
200
204
|
end
|
|
201
205
|
|
|
202
|
-
def collect_api_key_interactively
|
|
203
|
-
prompt = TTY::Prompt.new
|
|
204
|
-
|
|
206
|
+
def collect_api_key_interactively(server)
|
|
205
207
|
say "\nAPI key not found in .strata file", :yellow
|
|
206
|
-
say "\nTo deploy to Strata, you need an API key.\nYou can get your API key from:", :white
|
|
207
|
-
say " • Your Strata server dashboard (Settings → API Keys)\n • Your Strata administrator\n", :cyan
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
choice = prompt.select("How would you like to authenticate?") do |menu|
|
|
210
|
+
menu.choice "Sign in via browser (recommended)", :browser
|
|
211
|
+
menu.choice "Paste an API key", :manual
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
api_key = if choice == :browser
|
|
215
|
+
fetch_api_key_via_browser(server)
|
|
216
|
+
else
|
|
217
|
+
say "\nYou can get your API key from:", :white
|
|
218
|
+
say " • Your Strata server dashboard (Settings → API Keys)\n • Your Strata administrator\n", :cyan
|
|
219
|
+
|
|
220
|
+
prompt.mask("Enter your Strata API key:") do |q|
|
|
221
|
+
q.required true
|
|
222
|
+
q.validate(/\S+/, "API key cannot be empty")
|
|
223
|
+
end
|
|
212
224
|
end
|
|
213
225
|
|
|
214
226
|
# Save API key to .strata file
|
|
@@ -272,15 +284,15 @@ module Strata
|
|
|
272
284
|
raise Strata::CommandError, "Audit checks failed. Fix errors before deploying." if e.status != 0
|
|
273
285
|
end
|
|
274
286
|
|
|
275
|
-
def
|
|
287
|
+
def ensure_working_tree_clean
|
|
276
288
|
return unless Utils::Git.git_repo?
|
|
289
|
+
return unless Utils::Git.uncommitted_changes?
|
|
277
290
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
end
|
|
291
|
+
raise Strata::CommandError, "You have uncommitted changes. Please commit or stash them before deploying."
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def run_git_status_checks(branch_name)
|
|
295
|
+
return unless Utils::Git.git_repo?
|
|
284
296
|
|
|
285
297
|
# Fetch from remote
|
|
286
298
|
with_spinner("Fetching latest changes from remote") do
|
|
@@ -309,10 +321,16 @@ module Strata
|
|
|
309
321
|
commit_info = Utils::Git.commit_info
|
|
310
322
|
committer_info = Utils::Git.committer_info
|
|
311
323
|
|
|
312
|
-
# Get file modifications since last deployment
|
|
324
|
+
# Get file modifications since the last deployment. Without a valid baseline
|
|
325
|
+
# commit we can't compute an incremental diff, so send the full file list
|
|
326
|
+
# rather than silently narrowing to the last commit.
|
|
313
327
|
file_modifications = if last_deployment_commit
|
|
314
328
|
Utils::Git.changed_files_since(last_deployment_commit)
|
|
315
329
|
else
|
|
330
|
+
print_warning(
|
|
331
|
+
"No valid previous deployment commit to diff against — sending all files. " \
|
|
332
|
+
"Renames can't be auto-detected this deploy; verify renamed tables afterward or use a rename migration."
|
|
333
|
+
)
|
|
316
334
|
Utils::Git.file_modifications
|
|
317
335
|
end
|
|
318
336
|
|
|
@@ -411,7 +429,6 @@ module Strata
|
|
|
411
429
|
production_branch = get_production_branch(config)
|
|
412
430
|
return unless branch_id == production_branch
|
|
413
431
|
|
|
414
|
-
prompt = TTY::Prompt.new
|
|
415
432
|
message = "Deploying to production branch '#{branch_id}'. Continue? [y/N]"
|
|
416
433
|
return if prompt.yes?(message, default: false)
|
|
417
434
|
|
|
@@ -440,7 +457,10 @@ module Strata
|
|
|
440
457
|
|
|
441
458
|
commit_hash = last_deployment["commit"]
|
|
442
459
|
unless Utils::Git.validate_commit_hash(commit_hash)
|
|
443
|
-
|
|
460
|
+
print_warning(
|
|
461
|
+
"Last deployment commit #{commit_hash.inspect} is not a valid git SHA " \
|
|
462
|
+
"(likely a non-git deploy). Can't compute an incremental diff; falling back to a full-file deploy.\n"
|
|
463
|
+
)
|
|
444
464
|
return nil
|
|
445
465
|
end
|
|
446
466
|
print_info("Found last successful deployment at commit: #{commit_hash[0..7]}...\n")
|
|
@@ -4,15 +4,12 @@ require_relative "../guard"
|
|
|
4
4
|
require_relative "../terminal"
|
|
5
5
|
require_relative "../output"
|
|
6
6
|
require_relative "../helpers/project_helper"
|
|
7
|
+
require_relative "../helpers/browser_auth"
|
|
7
8
|
require_relative "../agent_mode"
|
|
8
9
|
require_relative "../api/client"
|
|
9
10
|
require "yaml"
|
|
10
11
|
require "fileutils"
|
|
11
12
|
require "tty-prompt"
|
|
12
|
-
require "net/http"
|
|
13
|
-
require "json"
|
|
14
|
-
require "securerandom"
|
|
15
|
-
require "rbconfig"
|
|
16
13
|
|
|
17
14
|
module Strata
|
|
18
15
|
module CLI
|
|
@@ -22,12 +19,12 @@ module Strata
|
|
|
22
19
|
include Terminal
|
|
23
20
|
include Output
|
|
24
21
|
include AgentMode
|
|
22
|
+
include Helpers::BrowserAuth
|
|
25
23
|
|
|
26
24
|
SAMPLE_REPO = "https://github.com/stratasite/tpc-ds-retail-sample"
|
|
27
25
|
SAMPLE_DIR = "tpc-ds-retail-sample"
|
|
28
26
|
SAMPLE_PROJECT_UID = "tpc-ds-retail-sample"
|
|
29
27
|
DEFAULT_SERVER = "http://localhost:8080"
|
|
30
|
-
AUTH_TIMEOUT_SECONDS = 300
|
|
31
28
|
|
|
32
29
|
desc "sample", "Clone the Strata sample project and set it up interactively"
|
|
33
30
|
option :api_key, aliases: ["a"], type: :string, desc: "Strata API key (optional — skip browser auth)"
|
|
@@ -37,7 +34,6 @@ module Strata
|
|
|
37
34
|
server = options[:server].delete_suffix("/")
|
|
38
35
|
api_key = options[:api_key]
|
|
39
36
|
else
|
|
40
|
-
prompt = TTY::Prompt.new
|
|
41
37
|
server = (options[:server] || prompt.ask("Strata server URL:", default: DEFAULT_SERVER) { |q| q.modify :strip }).delete_suffix("/")
|
|
42
38
|
api_key = fetch_api_key_via_browser(server)
|
|
43
39
|
end
|
|
@@ -68,42 +64,6 @@ module Strata
|
|
|
68
64
|
print_info(" Next: cd #{SAMPLE_DIR} && strata deploy")
|
|
69
65
|
end
|
|
70
66
|
|
|
71
|
-
no_commands do
|
|
72
|
-
def fetch_api_key_via_browser(server)
|
|
73
|
-
state = SecureRandom.hex(16)
|
|
74
|
-
auth_url = "#{server}/cli/auth?state=#{state}"
|
|
75
|
-
poll_url = "#{server}/api/v1/cli/auth/#{state}"
|
|
76
|
-
|
|
77
|
-
say "\nOpening your browser to sign in to Strata...", :cyan
|
|
78
|
-
open_browser(auth_url)
|
|
79
|
-
say "Waiting for authentication (you may close the browser tab once done)", :white
|
|
80
|
-
|
|
81
|
-
deadline = Time.now + AUTH_TIMEOUT_SECONDS
|
|
82
|
-
loop do
|
|
83
|
-
raise Strata::CommandError, "Authentication timed out. Run `strata project sample` to try again." if Time.now > deadline
|
|
84
|
-
|
|
85
|
-
begin
|
|
86
|
-
response = Net::HTTP.get_response(URI(poll_url))
|
|
87
|
-
return JSON.parse(response.body)["api_key"] if response.code == "200"
|
|
88
|
-
rescue Errno::ECONNREFUSED
|
|
89
|
-
raise Strata::CommandError, "Cannot reach server at #{server}. Is it running?"
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
print "."
|
|
93
|
-
$stdout.flush
|
|
94
|
-
sleep 1
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def open_browser(url)
|
|
99
|
-
case RbConfig::CONFIG["host_os"]
|
|
100
|
-
when /darwin/ then system("open", url)
|
|
101
|
-
when /linux/ then system("xdg-open", url)
|
|
102
|
-
when /mswin|mingw/ then system("start", "", url)
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
67
|
desc "link PROJECT_ID", "Link local project to an existing project on the server"
|
|
108
68
|
def link(project_id)
|
|
109
69
|
raise Strata::CommandError, "Project ID is required." unless project_id && !project_id.to_s.strip.empty?
|
data/lib/strata/cli/utils/git.rb
CHANGED
|
@@ -88,12 +88,17 @@ module Strata
|
|
|
88
88
|
}
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# The file-modifications payload to send when there is no valid baseline
|
|
92
|
+
# commit to diff against: the full list of tracked YAML model files, so no
|
|
93
|
+
# added/modified file is silently dropped. NOTE: renames cannot be detected
|
|
94
|
+
# without a baseline — in this mode a rename appears as add + delete.
|
|
91
95
|
def file_modifications
|
|
92
|
-
|
|
93
|
-
return [] unless diff_output
|
|
96
|
+
return [] unless git_repo?
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
output = run_git_command("ls-files", "*.yml", "*.yaml")
|
|
99
|
+
return [] unless output
|
|
100
|
+
|
|
101
|
+
output.lines.map(&:strip).reject(&:empty?).map { |path| ["M", path] }
|
|
97
102
|
end
|
|
98
103
|
|
|
99
104
|
def changed_files_since(commit_hash)
|
|
@@ -108,8 +113,9 @@ module Strata
|
|
|
108
113
|
_, _, status = Open3.capture3("git", "rev-parse", "--verify", "#{commit_hash}^{commit}")
|
|
109
114
|
return [] unless status.success?
|
|
110
115
|
|
|
111
|
-
# Get all changed files since that commit (including added, modified, deleted, renamed)
|
|
112
|
-
|
|
116
|
+
# Get all changed files since that commit (including added, modified, deleted, renamed).
|
|
117
|
+
# --find-renames makes rename (R) detection explicit rather than relying on git config.
|
|
118
|
+
diff_output = run_git_command("diff", "--name-status", "--find-renames", commit_hash, "HEAD")
|
|
113
119
|
return [] unless diff_output
|
|
114
120
|
|
|
115
121
|
modifications = parse_diff_output(diff_output)
|
|
@@ -130,7 +136,7 @@ module Strata
|
|
|
130
136
|
|
|
131
137
|
# Get file paths that changed (for added and modified files only, exclude deleted)
|
|
132
138
|
# --diff-filter=ACMR: Added, Copied, Modified, Renamed (excludes Deleted)
|
|
133
|
-
diff_output = run_git_command("diff", "--name-status", "--diff-filter=ACMR", commit_hash, "HEAD")
|
|
139
|
+
diff_output = run_git_command("diff", "--name-status", "--find-renames", "--diff-filter=ACMR", commit_hash, "HEAD")
|
|
134
140
|
return [] unless diff_output
|
|
135
141
|
|
|
136
142
|
paths = []
|
|
@@ -228,9 +234,9 @@ module Strata
|
|
|
228
234
|
end
|
|
229
235
|
|
|
230
236
|
def parse_diff_output(output)
|
|
231
|
-
output.lines.
|
|
237
|
+
output.lines.filter_map do |line|
|
|
232
238
|
parse_diff_status(line.strip)
|
|
233
|
-
end
|
|
239
|
+
end
|
|
234
240
|
end
|
|
235
241
|
|
|
236
242
|
def parse_diff_status(line)
|
|
@@ -314,6 +320,22 @@ module Strata
|
|
|
314
320
|
end
|
|
315
321
|
end
|
|
316
322
|
|
|
323
|
+
# Commits everything in a freshly initialized repo. Skips a repo that already
|
|
324
|
+
# has history, so running this inside an existing project never touches it.
|
|
325
|
+
# Returns :committed, :skipped or :failed (usually an unset user.name/email).
|
|
326
|
+
def initial_commit(message)
|
|
327
|
+
return :skipped unless git_repo?
|
|
328
|
+
|
|
329
|
+
_, _, head = Open3.capture3("git", "rev-parse", "--verify", "HEAD")
|
|
330
|
+
return :skipped if head.success?
|
|
331
|
+
|
|
332
|
+
_, _, add = Open3.capture3("git", "add", "-A")
|
|
333
|
+
return :failed unless add.success?
|
|
334
|
+
|
|
335
|
+
_, _, commit = Open3.capture3("git", "commit", "-m", message)
|
|
336
|
+
commit.success? ? :committed : :failed
|
|
337
|
+
end
|
|
338
|
+
|
|
317
339
|
def commit_file(file_path, commit_message, project_path = Dir.pwd)
|
|
318
340
|
return false unless git_repo?
|
|
319
341
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Vendored from strata (server) lib/sql/expression_parser.rb, restyled to standard — keep the logic in sync.
|
|
2
|
+
module Strata
|
|
3
|
+
module CLI
|
|
4
|
+
module Sql
|
|
5
|
+
class ExpressionParser
|
|
6
|
+
attr_reader :expression, :columns, :field_refs, :measure_refs, :dimension_refs, :functions
|
|
7
|
+
|
|
8
|
+
def initialize(expression, reserved: DWH.reserved_keywords, aggregates: DWH.aggregate_functions)
|
|
9
|
+
@expression = expression.downcase
|
|
10
|
+
@reserved = reserved
|
|
11
|
+
@aggregates = aggregates
|
|
12
|
+
@columns = []
|
|
13
|
+
@field_refs = []
|
|
14
|
+
@measure_refs = []
|
|
15
|
+
@dimension_refs = []
|
|
16
|
+
@functions = []
|
|
17
|
+
@wild = false
|
|
18
|
+
parse
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def wild?
|
|
22
|
+
@wild
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Does this expression apply an aggregate function (sum, max, count, ...)?
|
|
26
|
+
# Used to decide whether a calculation behaves as a measure.
|
|
27
|
+
def has_aggregate_functions?
|
|
28
|
+
@functions.any? { |f| @aggregates.include?(f) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Does the expression carry a window (`... over (...)`)? Windows are valid wherever the
|
|
32
|
+
# expression evaluates over final rows — including a non-grouped query — but can never
|
|
33
|
+
# sit inside a GROUP BY.
|
|
34
|
+
def has_window_functions?
|
|
35
|
+
!!@windowed
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The @d dimension references that sit outside every aggregate call. In an
|
|
39
|
+
# aggregating expression these are what SQL demands be grouped: a dimension
|
|
40
|
+
# inside sum(case when [Region]@d = 'x' …) is fine, one in
|
|
41
|
+
# case when [Region]@d is null then null else sum(…) end is not unless
|
|
42
|
+
# the query groups by Region.
|
|
43
|
+
def unaggregated_dimension_refs
|
|
44
|
+
@unaggregated_dimension_refs ||= begin
|
|
45
|
+
refs = []
|
|
46
|
+
stack = [] # one entry per open paren: is it an aggregate call?
|
|
47
|
+
# Bracketed references are consumed atomically FIRST, so a parenthesis inside a
|
|
48
|
+
# reference name ([Month(Date)]) cannot unbalance the stack.
|
|
49
|
+
remove_string_literals(@expression).scan(/\[([^\]]+)\]@d|\[[^\]]+\](?:@m)?|([a-z_][a-z0-9_]*)?\(|\)/) do
|
|
50
|
+
token = Regexp.last_match(0)
|
|
51
|
+
if Regexp.last_match(1)
|
|
52
|
+
ref = Regexp.last_match(1).strip
|
|
53
|
+
refs << ref unless stack.any? || refs.include?(ref)
|
|
54
|
+
elsif token.start_with?("[")
|
|
55
|
+
next # an @m or alias reference: opaque
|
|
56
|
+
elsif token == ")"
|
|
57
|
+
stack.pop
|
|
58
|
+
else
|
|
59
|
+
stack << @aggregates.include?(Regexp.last_match(2).to_s)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
refs
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
# Can this (resolved) SQL expression legally appear in a GROUP BY? Aggregates and
|
|
68
|
+
# windows evaluate over the grouped rows, so an expression carrying either cannot —
|
|
69
|
+
# the binder rejects it ("GROUP BY clause cannot contain aggregates").
|
|
70
|
+
def ungroupable?(sql)
|
|
71
|
+
parsed = parse(sql)
|
|
72
|
+
parsed.has_aggregate_functions? || parsed.has_window_functions?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def entity_gsub(sql, entity_name, replacement)
|
|
76
|
+
sql.gsub(/\[\s*#{Regexp.quote(entity_name)}\s*\](@[mdMD])?/i, replacement)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Qualify a column reference within sql with table_alias and adapter quoting.
|
|
80
|
+
# Handles both double-quoted identifiers ("limit") and bare identifiers (amount).
|
|
81
|
+
# The quoted form is replaced first; negative lookarounds on the bare-word pass
|
|
82
|
+
# prevent re-matching col that now appears inside the qualified replacement.
|
|
83
|
+
def column_gsub(sql, col, adapter, table_alias)
|
|
84
|
+
qualified = "#{table_alias}.#{adapter.quote(col)}"
|
|
85
|
+
sql = sql.gsub("\"#{col}\"", qualified)
|
|
86
|
+
sql.gsub(/(?<!")\b#{Regexp.quote(col)}\b(?!")/i, qualified)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse(expression, reserved: DWH.reserved_keywords, aggregates: DWH.aggregate_functions)
|
|
90
|
+
new(expression, reserved: reserved, aggregates: aggregates)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def parse
|
|
97
|
+
# Sanitize the expression first
|
|
98
|
+
sanitized = remove_string_literals(@expression)
|
|
99
|
+
|
|
100
|
+
matched_refs = []
|
|
101
|
+
# Extract field references in bracketed notation — BEFORE scanning functions, so a
|
|
102
|
+
# parenthesis inside a reference name ([Month(Date)]) cannot masquerade as a call.
|
|
103
|
+
sanitized.scan(/\[([^\]]+)\](@[md])?/).each do |ref, type|
|
|
104
|
+
matched_refs << "[#{ref}]#{type}"
|
|
105
|
+
case type
|
|
106
|
+
when "@m"
|
|
107
|
+
@measure_refs << ref.strip unless @measure_refs.include?(ref)
|
|
108
|
+
when "@d"
|
|
109
|
+
@dimension_refs << ref.strip unless @dimension_refs.include?(ref)
|
|
110
|
+
else
|
|
111
|
+
@field_refs << ref.strip unless @field_refs.include?(ref)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
sanitized = remove_matched_refs(sanitized, matched_refs)
|
|
116
|
+
|
|
117
|
+
# Extract functions
|
|
118
|
+
sanitized.scan(/\b([a-zA-Z_][a-zA-Z0-9_]*)\(/).each do |func|
|
|
119
|
+
@functions << func.first
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A window clause marks the expression as computed over the final rows — after any
|
|
123
|
+
# grouping — which is what has_window_functions? reports.
|
|
124
|
+
@windowed = sanitized.match?(/\bover\s*\(/)
|
|
125
|
+
|
|
126
|
+
# Extract quoted columns
|
|
127
|
+
@expression.scan(/"([^"]+)"/).flatten.each do |col|
|
|
128
|
+
@columns << col
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Extract column references (alphanumeric with optional underscores)
|
|
132
|
+
sanitized.scan(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b/).flatten.each do |col|
|
|
133
|
+
@columns << col unless @reserved.include?(col) || @functions.include?(col) || @columns.include?(col)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
@wild = true if sanitized.match?(/\w+\(\s?\*\s?\)/)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def remove_string_literals(expr)
|
|
140
|
+
expr.gsub(/'[^']*'/, "'__str__'")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def remove_matched_refs(sanitized, matched_refs)
|
|
144
|
+
l = sanitized
|
|
145
|
+
matched_refs.each do |ref|
|
|
146
|
+
l.gsub!(ref, "")
|
|
147
|
+
end
|
|
148
|
+
l
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
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.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ajo Abraham
|
|
@@ -293,11 +293,13 @@ files:
|
|
|
293
293
|
- lib/strata/cli/generators/templates/test.yml
|
|
294
294
|
- lib/strata/cli/generators/test.rb
|
|
295
295
|
- lib/strata/cli/guard.rb
|
|
296
|
+
- lib/strata/cli/helpers/browser_auth.rb
|
|
296
297
|
- lib/strata/cli/helpers/command_context.rb
|
|
297
298
|
- lib/strata/cli/helpers/datasource_helper.rb
|
|
298
299
|
- lib/strata/cli/helpers/description_helper.rb
|
|
299
300
|
- lib/strata/cli/helpers/project_helper.rb
|
|
300
301
|
- lib/strata/cli/helpers/prompts.rb
|
|
302
|
+
- lib/strata/cli/helpers/semantic_audit_checks.rb
|
|
301
303
|
- lib/strata/cli/helpers/table_filter.rb
|
|
302
304
|
- lib/strata/cli/main.rb
|
|
303
305
|
- lib/strata/cli/output.rb
|
|
@@ -316,6 +318,7 @@ files:
|
|
|
316
318
|
- lib/strata/cli/utils/deployment_monitor.rb
|
|
317
319
|
- lib/strata/cli/utils/git.rb
|
|
318
320
|
- lib/strata/cli/utils/import_manager.rb
|
|
321
|
+
- lib/strata/cli/utils/sql_expression_parser.rb
|
|
319
322
|
- lib/strata/cli/utils/test_reporter.rb
|
|
320
323
|
- lib/strata/cli/utils/version_checker.rb
|
|
321
324
|
- lib/strata/cli/utils/yaml_import_resolver.rb
|
|
@@ -340,7 +343,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
340
343
|
- !ruby/object:Gem::Version
|
|
341
344
|
version: '0'
|
|
342
345
|
requirements: []
|
|
343
|
-
rubygems_version: 3.6.
|
|
346
|
+
rubygems_version: 3.6.9
|
|
344
347
|
specification_version: 4
|
|
345
348
|
summary: Command-line interface for the Strata Semantic Analytics System
|
|
346
349
|
test_files: []
|