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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module ForestAdminDatasourceSnowflake
2
+ VERSION = "1.29.1"
3
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'forest_admin_datasource_snowflake/version'
2
+ require 'odbc'
3
+ require 'zeitwerk'
4
+
5
+ Zeitwerk::Loader.for_gem.setup
6
+
7
+ module ForestAdminDatasourceSnowflake
8
+ class Error < StandardError; end
9
+ 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: []