strata-cli 0.1.20 → 0.1.22
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 +56 -0
- data/README.md +26 -9
- data/lib/strata/cli/configuration.rb +11 -1
- data/lib/strata/cli/descriptions/init.txt +8 -7
- data/lib/strata/cli/generators/project.rb +6 -129
- data/lib/strata/cli/generators/templates/AGENTS.md +34 -34
- data/lib/strata/cli/generators/templates/project.yml +2 -2
- data/lib/strata/cli/generators/templates/table.table_name.yml +1 -1
- data/lib/strata/cli/helpers/datasource_helper.rb +19 -2
- data/lib/strata/cli/helpers/project_helper.rb +3 -2
- data/lib/strata/cli/helpers/prompts.rb +2 -2
- data/lib/strata/cli/helpers/semantic_audit_checks.rb +87 -29
- data/lib/strata/cli/main.rb +5 -7
- data/lib/strata/cli/sub_commands/audit.rb +94 -27
- data/lib/strata/cli/sub_commands/datasource.rb +6 -0
- data/lib/strata/cli/sub_commands/deploy.rb +1 -30
- data/lib/strata/cli/sub_commands/project.rb +6 -9
- data/lib/strata/cli/utils/archive.rb +3 -1
- data/lib/strata/cli/utils/sql_expression_parser.rb +53 -12
- data/lib/strata/cli/version.rb +1 -1
- metadata +2 -2
|
@@ -24,19 +24,15 @@ module Strata
|
|
|
24
24
|
SAMPLE_REPO = "https://github.com/stratasite/tpc-ds-retail-sample"
|
|
25
25
|
SAMPLE_DIR = "tpc-ds-retail-sample"
|
|
26
26
|
SAMPLE_PROJECT_UID = "tpc-ds-retail-sample"
|
|
27
|
-
DEFAULT_SERVER = "http://localhost:8080"
|
|
28
27
|
|
|
29
28
|
desc "sample", "Clone the Strata sample project and set it up interactively"
|
|
30
29
|
option :api_key, aliases: ["a"], type: :string, desc: "Strata API key (optional — skip browser auth)"
|
|
31
30
|
option :server, aliases: ["s"], type: :string, desc: "Strata server URL (optional — prompted if omitted)"
|
|
32
31
|
def sample
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
server = (options[:server] || prompt.ask("Strata server URL:", default: DEFAULT_SERVER) { |q| q.modify :strip }).delete_suffix("/")
|
|
38
|
-
api_key = fetch_api_key_via_browser(server)
|
|
39
|
-
end
|
|
32
|
+
server = Configuration.resolve_server(
|
|
33
|
+
options[:server] || prompt.ask("Strata server URL:", default: Configuration::DEFAULT_SERVER) { |q| q.modify :strip }
|
|
34
|
+
)
|
|
35
|
+
api_key = options[:api_key] || fetch_api_key_via_browser(server)
|
|
40
36
|
|
|
41
37
|
if File.directory?(SAMPLE_DIR)
|
|
42
38
|
print_info("Directory '#{SAMPLE_DIR}' already exists — re-using it")
|
|
@@ -51,9 +47,10 @@ module Strata
|
|
|
51
47
|
File.write(".gitignore", ".strata\n.strata_cli/\n")
|
|
52
48
|
|
|
53
49
|
strata_path = ".strata"
|
|
54
|
-
File.write(strata_path, "api_key: #{api_key}\
|
|
50
|
+
File.write(strata_path, "api_key: #{api_key}\n")
|
|
55
51
|
File.chmod(0o600, strata_path)
|
|
56
52
|
|
|
53
|
+
Helpers::ProjectHelper.persist_server_to_project_yml(server)
|
|
57
54
|
Helpers::ProjectHelper.persist_project_id_to_yml(SAMPLE_PROJECT_UID)
|
|
58
55
|
|
|
59
56
|
system("git add -A", exception: true)
|
|
@@ -34,7 +34,9 @@ module Strata
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
private_class_method def collect_yml_files(project_path)
|
|
37
|
-
|
|
37
|
+
# The server globs models as {yml,yaml} (deploy_job.rb:121,162), so a
|
|
38
|
+
# .yaml model has to ship too -- it used to be left out of the archive.
|
|
39
|
+
Dir.glob(File.join(project_path, "**", "*.{yml,yaml}")).select do |file_path|
|
|
38
40
|
File.file?(file_path)
|
|
39
41
|
end
|
|
40
42
|
end
|
|
@@ -5,7 +5,19 @@ module Strata
|
|
|
5
5
|
class ExpressionParser
|
|
6
6
|
attr_reader :expression, :columns, :field_refs, :measure_refs, :dimension_refs, :functions
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# The identifier quote for ANSI SQL and any adapter whose quote setting is
|
|
9
|
+
# bracket-style: [Name] is Strata's field-reference syntax, so an adapter
|
|
10
|
+
# that quotes with brackets (SQL Server) quotes identifiers with double
|
|
11
|
+
# quotes in expressions instead.
|
|
12
|
+
ANSI_QUOTE = '"@exp"'
|
|
13
|
+
|
|
14
|
+
# +quote+ is the adapter's identifier quote template from its DWH settings
|
|
15
|
+
# ("@exp" wrapped in the warehouse's quote characters). Only that quote
|
|
16
|
+
# marks an identifier: on a backtick warehouse (MySQL, BigQuery, ...) a
|
|
17
|
+
# double-quoted span is a string literal, not a column.
|
|
18
|
+
def initialize(expression, reserved: DWH.reserved_keywords, aggregates: DWH.aggregate_functions, quote: ANSI_QUOTE)
|
|
19
|
+
@open, @close = self.class.quote_pair(quote)
|
|
20
|
+
@raw = expression
|
|
9
21
|
@expression = expression.downcase
|
|
10
22
|
@reserved = reserved
|
|
11
23
|
@aggregates = aggregates
|
|
@@ -46,7 +58,7 @@ module Strata
|
|
|
46
58
|
stack = [] # one entry per open paren: is it an aggregate call?
|
|
47
59
|
# Bracketed references are consumed atomically FIRST, so a parenthesis inside a
|
|
48
60
|
# reference name ([Month(Date)]) cannot unbalance the stack.
|
|
49
|
-
|
|
61
|
+
scrub(@expression).scan(/\[([^\]]+)\]@d|\[[^\]]+\](?:@m)?|([a-z_][a-z0-9_]*)?\(|\)/) do
|
|
50
62
|
token = Regexp.last_match(0)
|
|
51
63
|
if Regexp.last_match(1)
|
|
52
64
|
ref = Regexp.last_match(1).strip
|
|
@@ -64,6 +76,22 @@ module Strata
|
|
|
64
76
|
end
|
|
65
77
|
|
|
66
78
|
class << self
|
|
79
|
+
# The opening and closing identifier quote characters an adapter's quote
|
|
80
|
+
# template ("@exp", `@exp`, [@exp]) wraps around a name. Bracket quoting
|
|
81
|
+
# is Strata's field-reference syntax, so it falls back to ANSI quotes.
|
|
82
|
+
def quote_pair(template)
|
|
83
|
+
template = template.to_s
|
|
84
|
+
template = ANSI_QUOTE if template.strip.empty? || template.start_with?("[")
|
|
85
|
+
open, close = template.split("@exp", 2)
|
|
86
|
+
[open, (close.nil? || close.empty?) ? open : close]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# String literals: single-quoted always, double-quoted too on a
|
|
90
|
+
# warehouse whose identifier quote is something else (MySQL, BigQuery).
|
|
91
|
+
def literal_regex(open)
|
|
92
|
+
(open == '"') ? /'(?:[^']|'')*'/ : /'(?:[^']|'')*'|"(?:[^"]|"")*"/
|
|
93
|
+
end
|
|
94
|
+
|
|
67
95
|
# Can this (resolved) SQL expression legally appear in a GROUP BY? Aggregates and
|
|
68
96
|
# windows evaluate over the grouped rows, so an expression carrying either cannot —
|
|
69
97
|
# the binder rejects it ("GROUP BY clause cannot contain aggregates").
|
|
@@ -86,16 +114,19 @@ module Strata
|
|
|
86
114
|
sql.gsub(/(?<!")\b#{Regexp.quote(col)}\b(?!")/i, qualified)
|
|
87
115
|
end
|
|
88
116
|
|
|
89
|
-
def parse(expression, reserved: DWH.reserved_keywords, aggregates: DWH.aggregate_functions)
|
|
90
|
-
new(expression, reserved: reserved, aggregates: aggregates)
|
|
117
|
+
def parse(expression, reserved: DWH.reserved_keywords, aggregates: DWH.aggregate_functions, quote: ANSI_QUOTE)
|
|
118
|
+
new(expression, reserved: reserved, aggregates: aggregates, quote: quote)
|
|
91
119
|
end
|
|
92
120
|
end
|
|
93
121
|
|
|
94
122
|
private
|
|
95
123
|
|
|
96
124
|
def parse
|
|
97
|
-
#
|
|
98
|
-
|
|
125
|
+
# Quoted identifiers are collected from the original text -- their case
|
|
126
|
+
# is part of the name -- and scrubbed out of the working copy along with
|
|
127
|
+
# string literals, so nothing inside them reads as a function or column.
|
|
128
|
+
@quoted_identifiers = []
|
|
129
|
+
sanitized = scrub(@raw) { |ident| @quoted_identifiers << ident unless @quoted_identifiers.include?(ident) }.downcase
|
|
99
130
|
|
|
100
131
|
matched_refs = []
|
|
101
132
|
# Extract field references in bracketed notation — BEFORE scanning functions, so a
|
|
@@ -123,10 +154,7 @@ module Strata
|
|
|
123
154
|
# grouping — which is what has_window_functions? reports.
|
|
124
155
|
@windowed = sanitized.match?(/\bover\s*\(/)
|
|
125
156
|
|
|
126
|
-
|
|
127
|
-
@expression.scan(/"([^"]+)"/).flatten.each do |col|
|
|
128
|
-
@columns << col
|
|
129
|
-
end
|
|
157
|
+
@columns.concat(@quoted_identifiers)
|
|
130
158
|
|
|
131
159
|
# Extract column references (alphanumeric with optional underscores)
|
|
132
160
|
sanitized.scan(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b/).flatten.each do |col|
|
|
@@ -136,8 +164,21 @@ module Strata
|
|
|
136
164
|
@wild = true if sanitized.match?(/\w+\(\s?\*\s?\)/)
|
|
137
165
|
end
|
|
138
166
|
|
|
139
|
-
|
|
140
|
-
|
|
167
|
+
# Replaces string literals with a placeholder literal and quoted
|
|
168
|
+
# identifiers with a numeric placeholder (yielding each identifier's
|
|
169
|
+
# name), leaving an expression whose remaining words are all bare
|
|
170
|
+
# references, keywords, or functions. Inside a quoted identifier a
|
|
171
|
+
# doubled quote character is an escaped quote.
|
|
172
|
+
def scrub(expr)
|
|
173
|
+
o, c = Regexp.quote(@open), Regexp.quote(@close)
|
|
174
|
+
expr.gsub(/#{self.class.literal_regex(@open)}|#{o}((?:[^#{c}]|#{c}#{c})+)#{c}/) do
|
|
175
|
+
if Regexp.last_match(1)
|
|
176
|
+
yield Regexp.last_match(1) if block_given?
|
|
177
|
+
"0"
|
|
178
|
+
else
|
|
179
|
+
"'__str__'"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
141
182
|
end
|
|
142
183
|
|
|
143
184
|
def remove_matched_refs(sanitized, matched_refs)
|
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.22
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ajo Abraham
|
|
@@ -348,7 +348,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
348
348
|
- !ruby/object:Gem::Version
|
|
349
349
|
version: '0'
|
|
350
350
|
requirements: []
|
|
351
|
-
rubygems_version: 3.6.
|
|
351
|
+
rubygems_version: 3.6.7
|
|
352
352
|
specification_version: 4
|
|
353
353
|
summary: Command-line interface for the Strata Semantic Analytics System
|
|
354
354
|
test_files: []
|