forest_admin_datasource_snowflake 1.29.1
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 +7 -0
- data/.rspec +3 -0
- data/LICENSE +674 -0
- data/Rakefile +5 -0
- data/forest_admin_datasource_snowflake.gemspec +37 -0
- data/lib/forest_admin_datasource_snowflake/collection.rb +141 -0
- data/lib/forest_admin_datasource_snowflake/datasource.rb +258 -0
- data/lib/forest_admin_datasource_snowflake/parser/column.rb +85 -0
- data/lib/forest_admin_datasource_snowflake/parser/relation.rb +35 -0
- data/lib/forest_admin_datasource_snowflake/utils/identifier.rb +14 -0
- data/lib/forest_admin_datasource_snowflake/utils/query.rb +181 -0
- data/lib/forest_admin_datasource_snowflake/version.rb +3 -0
- data/lib/forest_admin_datasource_snowflake.rb +9 -0
- metadata +103 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
module ForestAdminDatasourceSnowflake
|
|
2
|
+
module Utils
|
|
3
|
+
class Query
|
|
4
|
+
Operators = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Operators
|
|
5
|
+
ConditionTreeBranch = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeBranch
|
|
6
|
+
ConditionTreeLeaf = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeLeaf
|
|
7
|
+
|
|
8
|
+
def initialize(collection, projection: nil, filter: nil, aggregation: nil, limit: nil)
|
|
9
|
+
@collection = collection
|
|
10
|
+
@projection = projection
|
|
11
|
+
@filter = filter
|
|
12
|
+
@aggregation = aggregation
|
|
13
|
+
@limit = limit
|
|
14
|
+
@binds = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_sql
|
|
18
|
+
@binds = []
|
|
19
|
+
cols = @projection&.columns&.map { |c| q(c) }
|
|
20
|
+
cols = ['*'] if cols.nil? || cols.empty?
|
|
21
|
+
sql = "SELECT #{cols.join(", ")} FROM #{q(@collection.table_name)}"
|
|
22
|
+
|
|
23
|
+
where = build_where_clause(@filter&.condition_tree)
|
|
24
|
+
sql << " WHERE #{where}" if where
|
|
25
|
+
|
|
26
|
+
order_by = build_order_by(@filter&.sort)
|
|
27
|
+
sql << " ORDER BY #{order_by}" if order_by
|
|
28
|
+
|
|
29
|
+
if @filter&.page
|
|
30
|
+
limit = @filter.page.respond_to?(:limit) ? @filter.page.limit : nil
|
|
31
|
+
offset = @filter.page.respond_to?(:offset) ? @filter.page.offset : nil
|
|
32
|
+
sql << " LIMIT #{Integer(limit)}" if limit
|
|
33
|
+
sql << " OFFSET #{Integer(offset)}" if offset
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
[sql, @binds]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_aggregate_sql
|
|
40
|
+
@binds = []
|
|
41
|
+
op_expr = build_aggregation_expression(@aggregation)
|
|
42
|
+
|
|
43
|
+
group_cols = (@aggregation.groups || []).map { |g| g[:field] }
|
|
44
|
+
select_cols = [op_expr, *group_cols.map { |c| q(c) }]
|
|
45
|
+
sql = "SELECT #{select_cols.join(", ")} FROM #{q(@collection.table_name)}"
|
|
46
|
+
|
|
47
|
+
where = build_where_clause(@filter&.condition_tree)
|
|
48
|
+
sql << " WHERE #{where}" if where
|
|
49
|
+
|
|
50
|
+
sql << " GROUP BY #{group_cols.map { |c| q(c) }.join(", ")}" if group_cols.any?
|
|
51
|
+
sql << " LIMIT #{Integer(@limit)}" if @limit
|
|
52
|
+
|
|
53
|
+
[sql, @binds, group_cols]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def q(identifier)
|
|
59
|
+
Identifier.quote(identifier)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_aggregation_expression(aggregation)
|
|
63
|
+
op = aggregation.operation.to_s.upcase
|
|
64
|
+
field = aggregation.field
|
|
65
|
+
|
|
66
|
+
blank_field = field.nil? || field.to_s.empty?
|
|
67
|
+
|
|
68
|
+
case op
|
|
69
|
+
when 'COUNT'
|
|
70
|
+
blank_field ? 'COUNT(*)' : "COUNT(#{q(field)})"
|
|
71
|
+
when 'SUM', 'AVG', 'MIN', 'MAX'
|
|
72
|
+
raise ForestAdminDatasourceSnowflake::Error, "Aggregation '#{op}' requires a field" if blank_field
|
|
73
|
+
|
|
74
|
+
"#{op}(#{q(field)})"
|
|
75
|
+
else
|
|
76
|
+
raise ForestAdminDatasourceSnowflake::Error, "Unsupported aggregation operation: #{op}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_order_by(sort)
|
|
81
|
+
return nil if sort.nil? || sort.empty?
|
|
82
|
+
|
|
83
|
+
sort.map { |s| "#{q(s[:field])} #{s[:ascending] ? "ASC" : "DESC"}" }.join(', ')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_where_clause(node)
|
|
87
|
+
return nil if node.nil?
|
|
88
|
+
|
|
89
|
+
case node
|
|
90
|
+
when ConditionTreeBranch
|
|
91
|
+
fragments = node.conditions.filter_map { |child| build_where_clause(child) }
|
|
92
|
+
return nil if fragments.empty?
|
|
93
|
+
|
|
94
|
+
joiner = node.aggregator.to_s.upcase == 'OR' ? ' OR ' : ' AND '
|
|
95
|
+
"(#{fragments.join(joiner)})"
|
|
96
|
+
when ConditionTreeLeaf
|
|
97
|
+
translate_leaf(node)
|
|
98
|
+
else
|
|
99
|
+
raise ForestAdminDatasourceSnowflake::Error, "Unsupported condition tree node: #{node.class}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def translate_leaf(leaf)
|
|
104
|
+
field = q(leaf.field)
|
|
105
|
+
case leaf.operator
|
|
106
|
+
when Operators::EQUAL then translate_equality(field, leaf.value)
|
|
107
|
+
when Operators::NOT_EQUAL then translate_equality(field, leaf.value, negate: true)
|
|
108
|
+
when Operators::LESS_THAN then bind!(leaf.value)
|
|
109
|
+
"#{field} < ?"
|
|
110
|
+
when Operators::GREATER_THAN then bind!(leaf.value)
|
|
111
|
+
"#{field} > ?"
|
|
112
|
+
when Operators::LESS_THAN_OR_EQUAL then bind!(leaf.value)
|
|
113
|
+
"#{field} <= ?"
|
|
114
|
+
when Operators::GREATER_THAN_OR_EQUAL then bind!(leaf.value)
|
|
115
|
+
"#{field} >= ?"
|
|
116
|
+
when Operators::IN then translate_in(field, leaf.value)
|
|
117
|
+
when Operators::NOT_IN then translate_in(field, leaf.value, negate: true)
|
|
118
|
+
when Operators::PRESENT then "#{field} IS NOT NULL"
|
|
119
|
+
when Operators::MISSING then "#{field} IS NULL"
|
|
120
|
+
when Operators::BLANK then "(#{field} IS NULL OR #{field} = '')"
|
|
121
|
+
when Operators::CONTAINS then translate_like(field, "%#{leaf.value}%")
|
|
122
|
+
when Operators::I_CONTAINS then translate_ilike(field, "%#{leaf.value}%")
|
|
123
|
+
when Operators::NOT_CONTAINS then translate_like(field, "%#{leaf.value}%", negate: true)
|
|
124
|
+
when Operators::NOT_I_CONTAINS then translate_ilike(field, "%#{leaf.value}%", negate: true)
|
|
125
|
+
when Operators::STARTS_WITH then translate_like(field, "#{leaf.value}%")
|
|
126
|
+
when Operators::I_STARTS_WITH then translate_ilike(field, "#{leaf.value}%")
|
|
127
|
+
when Operators::ENDS_WITH then translate_like(field, "%#{leaf.value}")
|
|
128
|
+
when Operators::I_ENDS_WITH then translate_ilike(field, "%#{leaf.value}")
|
|
129
|
+
when Operators::LIKE then translate_like(field, leaf.value)
|
|
130
|
+
when Operators::I_LIKE then translate_ilike(field, leaf.value)
|
|
131
|
+
when Operators::SHORTER_THAN then bind!(leaf.value)
|
|
132
|
+
"LENGTH(#{field}) < ?"
|
|
133
|
+
when Operators::LONGER_THAN then bind!(leaf.value)
|
|
134
|
+
"LENGTH(#{field}) > ?"
|
|
135
|
+
else
|
|
136
|
+
raise ForestAdminDatasourceSnowflake::Error,
|
|
137
|
+
"Unsupported operator '#{leaf.operator}' on field '#{leaf.field}'"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def translate_equality(quoted_field, value, negate: false)
|
|
142
|
+
return "#{quoted_field} #{negate ? "IS NOT NULL" : "IS NULL"}" if value.nil?
|
|
143
|
+
|
|
144
|
+
bind!(value)
|
|
145
|
+
"#{quoted_field} #{negate ? "<>" : "="} ?"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def translate_in(quoted_field, values, negate: false)
|
|
149
|
+
list = Array(values)
|
|
150
|
+
return negate ? '1=1' : '1=0' if list.empty?
|
|
151
|
+
|
|
152
|
+
non_nils = list.compact
|
|
153
|
+
null_term = "#{quoted_field} #{negate ? "IS NOT NULL" : "IS NULL"}"
|
|
154
|
+
return null_term if non_nils.empty?
|
|
155
|
+
|
|
156
|
+
placeholders = non_nils.map do |v|
|
|
157
|
+
bind!(v)
|
|
158
|
+
'?'
|
|
159
|
+
end.join(', ')
|
|
160
|
+
in_term = "#{quoted_field} #{negate ? "NOT IN" : "IN"} (#{placeholders})"
|
|
161
|
+
return in_term if non_nils.size == list.size
|
|
162
|
+
|
|
163
|
+
"(#{null_term}#{negate ? " AND " : " OR "}#{in_term})"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def translate_like(quoted_field, value, negate: false)
|
|
167
|
+
bind!(value)
|
|
168
|
+
"#{quoted_field} #{negate ? "NOT LIKE" : "LIKE"} ?"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def translate_ilike(quoted_field, value, negate: false)
|
|
172
|
+
bind!(value)
|
|
173
|
+
"LOWER(#{quoted_field}) #{negate ? "NOT LIKE" : "LIKE"} LOWER(?)"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def bind!(value)
|
|
177
|
+
@binds << value
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: forest_admin_datasource_snowflake
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.29.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Forest Admin
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: connection_pool
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.4'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.4'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: ruby-odbc
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.99999'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.99999'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: zeitwerk
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.3'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.3'
|
|
55
|
+
description: Exposes Snowflake tables and views as Forest Admin collections via ODBC.
|
|
56
|
+
Queries are translated from Forest ConditionTrees to parameterized SQL; no ActiveRecord
|
|
57
|
+
involvement on the Snowflake side.
|
|
58
|
+
email:
|
|
59
|
+
- support@forestadmin.com
|
|
60
|
+
executables: []
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- ".rspec"
|
|
65
|
+
- LICENSE
|
|
66
|
+
- Rakefile
|
|
67
|
+
- forest_admin_datasource_snowflake.gemspec
|
|
68
|
+
- lib/forest_admin_datasource_snowflake.rb
|
|
69
|
+
- lib/forest_admin_datasource_snowflake/collection.rb
|
|
70
|
+
- lib/forest_admin_datasource_snowflake/datasource.rb
|
|
71
|
+
- lib/forest_admin_datasource_snowflake/parser/column.rb
|
|
72
|
+
- lib/forest_admin_datasource_snowflake/parser/relation.rb
|
|
73
|
+
- lib/forest_admin_datasource_snowflake/utils/identifier.rb
|
|
74
|
+
- lib/forest_admin_datasource_snowflake/utils/query.rb
|
|
75
|
+
- lib/forest_admin_datasource_snowflake/version.rb
|
|
76
|
+
homepage: https://www.forestadmin.com
|
|
77
|
+
licenses:
|
|
78
|
+
- GPL-3.0
|
|
79
|
+
metadata:
|
|
80
|
+
homepage_uri: https://www.forestadmin.com
|
|
81
|
+
source_code_uri: https://github.com/ForestAdmin/agent-ruby
|
|
82
|
+
changelog_uri: https://github.com/ForestAdmin/agent-ruby/blob/main/CHANGELOG.md
|
|
83
|
+
rubygems_mfa_required: 'false'
|
|
84
|
+
post_install_message:
|
|
85
|
+
rdoc_options: []
|
|
86
|
+
require_paths:
|
|
87
|
+
- lib
|
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: 3.0.0
|
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '0'
|
|
98
|
+
requirements: []
|
|
99
|
+
rubygems_version: 3.4.20
|
|
100
|
+
signing_key:
|
|
101
|
+
specification_version: 4
|
|
102
|
+
summary: Snowflake datasource for Forest Admin (read-only).
|
|
103
|
+
test_files: []
|