multiwoven-integrations 0.42.0 → 0.43.0
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/lib/multiwoven/integrations/rollout.rb +2 -1
- data/lib/multiwoven/integrations/source/sql_server/client.rb +280 -0
- data/lib/multiwoven/integrations/source/sql_server/config/meta.json +16 -0
- data/lib/multiwoven/integrations/source/sql_server/config/spec.json +94 -0
- data/lib/multiwoven/integrations/source/sql_server/icon.svg +22 -0
- data/lib/multiwoven/integrations.rb +1 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62e0ad2ac6fc880f4b88dd4a511312b240645d5d1134966659a16225a927848d
|
|
4
|
+
data.tar.gz: a7368698f3dd5a28894549b635c136b348d43feb25513a1a93f8333a6f9c5ae7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0a58e7df26e7579cf5037d6b6605374695a07c6276279da1bd4b9582d7c9f797a001eb9ab7b52e41f402468c7e43c0958d1d1882f27f66aa92617e9a6bd8058
|
|
7
|
+
data.tar.gz: 8867babc43650be6e51e1a40c129089fa1b2ba46b561967b6d58bf251b3c48d3e42ef1d21ac46b5731d3a44012ffbd5edba298d76d621b3422db06f487b8eb5c
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Multiwoven
|
|
4
4
|
module Integrations
|
|
5
|
-
VERSION = "0.
|
|
5
|
+
VERSION = "0.43.0"
|
|
6
6
|
|
|
7
7
|
ENABLED_SOURCES = %w[
|
|
8
8
|
Snowflake
|
|
@@ -45,6 +45,7 @@ module Multiwoven
|
|
|
45
45
|
OneDrive
|
|
46
46
|
MicrosoftDynamics
|
|
47
47
|
EpicFhir
|
|
48
|
+
SqlServer
|
|
48
49
|
].freeze
|
|
49
50
|
|
|
50
51
|
ENABLED_DESTINATIONS = %w[
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tiny_tds"
|
|
4
|
+
|
|
5
|
+
module Multiwoven::Integrations::Source
|
|
6
|
+
module SqlServer
|
|
7
|
+
include Multiwoven::Integrations::Core
|
|
8
|
+
class Client < SourceConnector
|
|
9
|
+
def check_connection(connection_config)
|
|
10
|
+
connection_config = connection_config.with_indifferent_access
|
|
11
|
+
db = create_connection(connection_config)
|
|
12
|
+
ConnectionStatus.new(
|
|
13
|
+
status: ConnectionStatusType["succeeded"]
|
|
14
|
+
).to_multiwoven_message
|
|
15
|
+
rescue TinyTds::Error => e
|
|
16
|
+
ConnectionStatus.new(
|
|
17
|
+
status: ConnectionStatusType["failed"],
|
|
18
|
+
message: e.message
|
|
19
|
+
).to_multiwoven_message
|
|
20
|
+
ensure
|
|
21
|
+
db&.close
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def discover(connection_config)
|
|
25
|
+
connection_config = connection_config.with_indifferent_access
|
|
26
|
+
schema = connection_config[:schema].presence || "dbo"
|
|
27
|
+
query = "SELECT table_name, column_name, data_type, is_nullable
|
|
28
|
+
FROM information_schema.columns
|
|
29
|
+
WHERE table_schema = '#{escape(schema)}'
|
|
30
|
+
ORDER BY table_name, ordinal_position;"
|
|
31
|
+
|
|
32
|
+
db = create_connection(connection_config)
|
|
33
|
+
records = db.execute(query).map do |row|
|
|
34
|
+
row.transform_keys { |key| key.to_s.downcase }
|
|
35
|
+
end
|
|
36
|
+
catalog = Catalog.new(streams: create_streams(records))
|
|
37
|
+
catalog.to_multiwoven_message
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
handle_exception(e, {
|
|
40
|
+
context: "SQLSERVER:DISCOVER:EXCEPTION",
|
|
41
|
+
type: "error"
|
|
42
|
+
})
|
|
43
|
+
ensure
|
|
44
|
+
db&.close
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def read(sync_config)
|
|
48
|
+
connection_config = sync_config.source.connection_specification.with_indifferent_access
|
|
49
|
+
@pagination_primary_key = sync_config.model.primary_key
|
|
50
|
+
query = sync_config.model.query
|
|
51
|
+
query = batched_query(query, sync_config.limit, sync_config.offset) unless sync_config.limit.nil? && sync_config.offset.nil?
|
|
52
|
+
|
|
53
|
+
db = create_connection(connection_config)
|
|
54
|
+
|
|
55
|
+
query(db, query)
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
handle_exception(e, {
|
|
58
|
+
context: "SQLSERVER:READ:EXCEPTION",
|
|
59
|
+
type: "error",
|
|
60
|
+
sync_id: sync_config.sync_id,
|
|
61
|
+
sync_run_id: sync_config.sync_run_id
|
|
62
|
+
})
|
|
63
|
+
ensure
|
|
64
|
+
@pagination_primary_key = nil
|
|
65
|
+
db&.close
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def search(vector_search_config)
|
|
69
|
+
connection_config = vector_search_config.source.connection_specification.with_indifferent_access
|
|
70
|
+
query = vector_search_config[:vector]
|
|
71
|
+
limit = vector_search_config[:limit]
|
|
72
|
+
query = batched_query(query, limit, 0) unless limit.nil?
|
|
73
|
+
|
|
74
|
+
db = create_connection(connection_config)
|
|
75
|
+
query(db, query)
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
handle_exception(e, {
|
|
78
|
+
context: "SQLSERVER:SEARCH:EXCEPTION",
|
|
79
|
+
type: "error"
|
|
80
|
+
})
|
|
81
|
+
ensure
|
|
82
|
+
db&.close
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def query(connection, sql)
|
|
88
|
+
connection.execute(reformat_query(sql)).map do |row|
|
|
89
|
+
RecordMessage.new(data: row, emitted_at: Time.now.to_i).to_multiwoven_message
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# SQL Server does not support LIMIT/OFFSET; use OFFSET/FETCH NEXT.
|
|
94
|
+
def batched_query(sql_query, limit, offset)
|
|
95
|
+
offset = offset.to_i
|
|
96
|
+
limit = limit&.to_i
|
|
97
|
+
raise ArgumentError, "Offset and limit must be non-negative" if offset.negative? || (!limit.nil? && limit.negative?)
|
|
98
|
+
|
|
99
|
+
sql_query = strip_trailing_terminator(sql_query)
|
|
100
|
+
raise ArgumentError, "Query already contains a LIMIT clause" if clause_outside_literals?(sql_query, /\bLIMIT\s+\d+\b/i)
|
|
101
|
+
raise ArgumentError, "Query already contains an OFFSET clause" if clause_outside_literals?(sql_query, /\bOFFSET\s+\d+\b/i)
|
|
102
|
+
|
|
103
|
+
apply_pagination(sql_query, limit, offset)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Convert Postgres/MySQL-style LIMIT/OFFSET (from upstream, including query_source) into SQL Server pagination.
|
|
107
|
+
def reformat_query(sql_query)
|
|
108
|
+
sql_query = strip_trailing_terminator(sql_query)
|
|
109
|
+
|
|
110
|
+
return sql_query if clause_outside_literals?(sql_query, /\bOFFSET\s+\d+\s+ROWS\b/i)
|
|
111
|
+
|
|
112
|
+
limit = nil
|
|
113
|
+
offset = nil
|
|
114
|
+
|
|
115
|
+
normalized = with_masked_literals(sql_query) do |masked|
|
|
116
|
+
if (match = masked.match(/\bLIMIT\s+(\d+)\b/i))
|
|
117
|
+
limit = match[1].to_i
|
|
118
|
+
masked = masked.sub(/\bLIMIT\s+\d+\b/i, "")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if (match = masked.match(/\bOFFSET\s+(\d+)\b(?!\s+ROWS)/i))
|
|
122
|
+
offset = match[1].to_i
|
|
123
|
+
masked = masked.sub(/\bOFFSET\s+\d+\b(?!\s+ROWS)/i, "")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
masked.gsub(/[^\S\n]+/, " ").strip
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
return normalized if limit.nil? && offset.nil?
|
|
130
|
+
|
|
131
|
+
apply_pagination(normalized, limit, offset || 0)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def apply_pagination(sql_query, limit, offset)
|
|
135
|
+
raise ArgumentError, "Limit must be at least 1" if !limit.nil? && limit.to_i < 1
|
|
136
|
+
|
|
137
|
+
keys = normalize_primary_keys(@pagination_primary_key)
|
|
138
|
+
has_order = clause_outside_literals?(sql_query, /\bORDER\s+BY\b/i)
|
|
139
|
+
|
|
140
|
+
# query_source appends LIMIT without ORDER BY or primary_key metadata.
|
|
141
|
+
# TOP does not require ORDER BY; OFFSET/FETCH does.
|
|
142
|
+
return apply_top(sql_query, limit) if offset.to_i.zero? && !limit.nil? && !has_order && keys.empty?
|
|
143
|
+
|
|
144
|
+
sql_query = ensure_stable_order(sql_query, keys: keys, has_order: has_order)
|
|
145
|
+
|
|
146
|
+
clause = "OFFSET #{offset.to_i} ROWS"
|
|
147
|
+
clause = "#{clause} FETCH NEXT #{limit.to_i} ROWS ONLY" unless limit.nil?
|
|
148
|
+
|
|
149
|
+
"#{sql_query} #{clause}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def apply_top(sql_query, limit)
|
|
153
|
+
with_masked_literals(sql_query) do |masked|
|
|
154
|
+
raise ArgumentError, "Query already contains TOP" if masked.match?(/\A\s*SELECT\s+(?:(?:DISTINCT|ALL)\s+)?TOP\b/i)
|
|
155
|
+
|
|
156
|
+
pattern = /\A\s*SELECT\s+((?:DISTINCT|ALL)\s+)?/i
|
|
157
|
+
raise ArgumentError, "Cannot apply TOP to this query; add ORDER BY" unless masked.match?(pattern)
|
|
158
|
+
|
|
159
|
+
masked.sub(pattern) { "SELECT #{Regexp.last_match(1)}TOP #{limit.to_i} " }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def ensure_stable_order(sql_query, keys:, has_order:)
|
|
164
|
+
return append_order_tie_breakers(sql_query, keys) if has_order
|
|
165
|
+
|
|
166
|
+
if keys.empty?
|
|
167
|
+
raise ArgumentError,
|
|
168
|
+
"Paginated SQL Server queries require an ORDER BY with a unique key, or a model primary_key"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
"#{sql_query} ORDER BY #{order_by_list(keys)}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def append_order_tie_breakers(sql_query, keys)
|
|
175
|
+
missing = keys.reject { |key| order_by_includes_key?(sql_query, key) }
|
|
176
|
+
return sql_query if missing.empty?
|
|
177
|
+
|
|
178
|
+
"#{sql_query}, #{order_by_list(missing)}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def order_by_includes_key?(sql_query, key)
|
|
182
|
+
included = false
|
|
183
|
+
with_masked_literals(sql_query) do |masked|
|
|
184
|
+
order_clause = masked[/\bORDER\s+BY\b(.+)\z/im, 1]
|
|
185
|
+
if order_clause
|
|
186
|
+
quoted = Regexp.escape(quote_identifier(key))
|
|
187
|
+
bare = Regexp.escape(key.to_s)
|
|
188
|
+
included = order_clause.match?(/#{quoted}|\b#{bare}\b/i)
|
|
189
|
+
end
|
|
190
|
+
masked
|
|
191
|
+
end
|
|
192
|
+
included
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def normalize_primary_keys(primary_key)
|
|
196
|
+
Array(primary_key).flat_map { |key| key.to_s.split(",") }.map(&:strip).reject(&:empty?).uniq
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def order_by_list(keys)
|
|
200
|
+
keys.map { |key| quote_identifier(key) }.join(", ")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def quote_identifier(name)
|
|
204
|
+
"[#{name.to_s.gsub("]", "]]")}]"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def strip_trailing_terminator(sql_query)
|
|
208
|
+
with_masked_literals(sql_query.to_s.strip) do |masked|
|
|
209
|
+
masked = masked.rstrip
|
|
210
|
+
masked = masked.chomp(";") while masked.end_with?(";")
|
|
211
|
+
masked
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def clause_outside_literals?(sql_query, pattern)
|
|
216
|
+
matched = false
|
|
217
|
+
with_masked_literals(sql_query) do |masked|
|
|
218
|
+
matched = masked.match?(pattern)
|
|
219
|
+
masked
|
|
220
|
+
end
|
|
221
|
+
matched
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Mask single-quoted SQL literals (including N'...' and escaped '') so rewrites
|
|
225
|
+
# do not touch values like 'a;b' or 'LIMIT 1'.
|
|
226
|
+
def with_masked_literals(sql_query)
|
|
227
|
+
literals = []
|
|
228
|
+
masked = sql_query.to_s.gsub(/(?:N)?'(?:''|[^'])*'/i) do |literal|
|
|
229
|
+
literals << literal
|
|
230
|
+
"__SQL_LITERAL_#{literals.length - 1}__"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
result = yield(masked)
|
|
234
|
+
literals.each_with_index do |literal, index|
|
|
235
|
+
result = result.gsub("__SQL_LITERAL_#{index}__", literal)
|
|
236
|
+
end
|
|
237
|
+
result
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def create_connection(connection_config)
|
|
241
|
+
raise "Unsupported Auth type" unless connection_config[:credentials][:auth_type] == "username/password"
|
|
242
|
+
|
|
243
|
+
TinyTds::Client.new(
|
|
244
|
+
username: connection_config[:credentials][:username],
|
|
245
|
+
password: connection_config[:credentials][:password],
|
|
246
|
+
host: connection_config[:host],
|
|
247
|
+
port: connection_config[:port].presence || 1433,
|
|
248
|
+
database: connection_config[:database],
|
|
249
|
+
timeout: 10,
|
|
250
|
+
azure: connection_config[:azure].presence || false
|
|
251
|
+
)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def create_streams(records)
|
|
255
|
+
group_by_table(records).map do |r|
|
|
256
|
+
Multiwoven::Integrations::Protocol::Stream.new(name: r[:tablename], action: StreamAction["fetch"], json_schema: convert_to_json_schema(r[:columns]))
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def group_by_table(records)
|
|
261
|
+
records.group_by { |entry| entry["table_name"] }.map do |table_name, columns|
|
|
262
|
+
{
|
|
263
|
+
tablename: table_name,
|
|
264
|
+
columns: columns.map do |column|
|
|
265
|
+
{
|
|
266
|
+
column_name: column["column_name"],
|
|
267
|
+
type: column["data_type"],
|
|
268
|
+
optional: column["is_nullable"] == "YES"
|
|
269
|
+
}
|
|
270
|
+
end
|
|
271
|
+
}
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def escape(value)
|
|
276
|
+
value.to_s.gsub("'", "''")
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"data": {
|
|
3
|
+
"name": "SqlServer",
|
|
4
|
+
"title": "SQL Server",
|
|
5
|
+
"connector_type": "source",
|
|
6
|
+
"category": "Data Warehouse",
|
|
7
|
+
"sub_category": "Relational Database",
|
|
8
|
+
"documentation_url": "https://docs.squared.ai/guides/sources/data-sources/sql-server",
|
|
9
|
+
"github_issue_label": "source-sql-server",
|
|
10
|
+
"icon": "https://res.cloudinary.com/dspflukeu/image/upload/v1790358609/Multiwoven/connectors/sql_server/icon.svg",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"release_stage": "alpha",
|
|
13
|
+
"support_level": "community",
|
|
14
|
+
"tags": ["language:ruby", "multiwoven"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"documentation_url": "https://docs.squared.ai/guides/sources/data-sources/sql-server",
|
|
3
|
+
"stream_type": "dynamic",
|
|
4
|
+
"connector_query_type": "raw_sql",
|
|
5
|
+
"connection_specification": {
|
|
6
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
7
|
+
"title": "SqlServer",
|
|
8
|
+
"type": "object",
|
|
9
|
+
"required": ["data_type", "credentials", "host", "database", "schema"],
|
|
10
|
+
"properties": {
|
|
11
|
+
"data_type": {
|
|
12
|
+
"description": "Type of data in the database",
|
|
13
|
+
"type": "string",
|
|
14
|
+
"title": "Data Format Type",
|
|
15
|
+
"oneOf": [
|
|
16
|
+
{
|
|
17
|
+
"const": "structured",
|
|
18
|
+
"title": "Tables & Records (Structured)"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"const": "vector",
|
|
22
|
+
"title": "Tables & Records containing Vector fields"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"default": "structured",
|
|
26
|
+
"order": 0
|
|
27
|
+
},
|
|
28
|
+
"credentials": {
|
|
29
|
+
"title": "",
|
|
30
|
+
"type": "object",
|
|
31
|
+
"required": ["auth_type", "username", "password"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"auth_type": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"default": "username/password",
|
|
36
|
+
"order": 0,
|
|
37
|
+
"readOnly": true
|
|
38
|
+
},
|
|
39
|
+
"username": {
|
|
40
|
+
"description": "Username refers to your individual SQL Server login credentials. At a minimum, the user associated with these credentials must be granted read access to the data intended for synchronization.",
|
|
41
|
+
"examples": ["SQLSERVER_USER"],
|
|
42
|
+
"type": "string",
|
|
43
|
+
"title": "Username",
|
|
44
|
+
"order": 1
|
|
45
|
+
},
|
|
46
|
+
"password": {
|
|
47
|
+
"description": "This field requires the password associated with the SQL Server login specified in the preceding section.",
|
|
48
|
+
"type": "string",
|
|
49
|
+
"multiwoven_secret": true,
|
|
50
|
+
"title": "Password",
|
|
51
|
+
"order": 2
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"order": 1
|
|
55
|
+
},
|
|
56
|
+
"host": {
|
|
57
|
+
"description": "The hostname or IP address of your SQL Server instance.",
|
|
58
|
+
"examples": ["127.0.0.1"],
|
|
59
|
+
"type": "string",
|
|
60
|
+
"title": "Host",
|
|
61
|
+
"order": 2
|
|
62
|
+
},
|
|
63
|
+
"port": {
|
|
64
|
+
"description": "The port number for your SQL Server instance, which defaults to 1433 and may vary based on your configuration.",
|
|
65
|
+
"examples": ["1433"],
|
|
66
|
+
"type": "string",
|
|
67
|
+
"title": "Port",
|
|
68
|
+
"order": 3
|
|
69
|
+
},
|
|
70
|
+
"database": {
|
|
71
|
+
"description": "The specific SQL Server database to connect to.",
|
|
72
|
+
"examples": ["SQLSERVER_DB"],
|
|
73
|
+
"type": "string",
|
|
74
|
+
"title": "Database",
|
|
75
|
+
"order": 4
|
|
76
|
+
},
|
|
77
|
+
"schema": {
|
|
78
|
+
"description": "The schema within the SQL Server database, typically 'dbo'.",
|
|
79
|
+
"examples": ["dbo"],
|
|
80
|
+
"default": "dbo",
|
|
81
|
+
"type": "string",
|
|
82
|
+
"title": "Schema",
|
|
83
|
+
"order": 5
|
|
84
|
+
},
|
|
85
|
+
"azure": {
|
|
86
|
+
"description": "Enable when connecting to Azure SQL Database (required for Azure encryption/login behaviour).",
|
|
87
|
+
"type": "boolean",
|
|
88
|
+
"title": "Azure SQL",
|
|
89
|
+
"default": false,
|
|
90
|
+
"order": 6
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -113,6 +113,7 @@ require_relative "integrations/source/aisquared/client"
|
|
|
113
113
|
require_relative "integrations/source/one_drive/client"
|
|
114
114
|
require_relative "integrations/source/microsoft_dynamics/client"
|
|
115
115
|
require_relative "integrations/source/epic_fhir/client"
|
|
116
|
+
require_relative "integrations/source/sql_server/client"
|
|
116
117
|
|
|
117
118
|
# Destination
|
|
118
119
|
require_relative "integrations/destination/klaviyo/client"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: multiwoven-integrations
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.43.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Subin T P
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -913,6 +913,10 @@ files:
|
|
|
913
913
|
- lib/multiwoven/integrations/source/snowflake/config/meta.json
|
|
914
914
|
- lib/multiwoven/integrations/source/snowflake/config/spec.json
|
|
915
915
|
- lib/multiwoven/integrations/source/snowflake/icon.svg
|
|
916
|
+
- lib/multiwoven/integrations/source/sql_server/client.rb
|
|
917
|
+
- lib/multiwoven/integrations/source/sql_server/config/meta.json
|
|
918
|
+
- lib/multiwoven/integrations/source/sql_server/config/spec.json
|
|
919
|
+
- lib/multiwoven/integrations/source/sql_server/icon.svg
|
|
916
920
|
- lib/multiwoven/integrations/source/watsonx_ai/client.rb
|
|
917
921
|
- lib/multiwoven/integrations/source/watsonx_ai/config/catalog.json
|
|
918
922
|
- lib/multiwoven/integrations/source/watsonx_ai/config/meta.json
|