wowsql-sdk 1.3.0 → 3.0.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 +4 -4
- data/README.md +359 -417
- data/lib/wowmysql.rb +3 -2
- data/lib/wowsql/auth.rb +313 -164
- data/lib/wowsql/client.rb +54 -25
- data/lib/wowsql/exceptions.rb +15 -14
- data/lib/wowsql/query_builder.rb +229 -120
- data/lib/wowsql/schema.rb +250 -0
- data/lib/wowsql/storage.rb +327 -176
- data/lib/wowsql/table.rb +111 -10
- data/lib/wowsql/version.rb +1 -1
- data/lib/wowsql.rb +2 -2
- metadata +20 -5
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
require 'json'
|
|
3
|
+
require_relative 'exceptions'
|
|
4
|
+
|
|
5
|
+
module WOWSQL
|
|
6
|
+
# Schema management client for PostgreSQL.
|
|
7
|
+
#
|
|
8
|
+
# Requires a SERVICE ROLE key (wowsql_service_...), not an anonymous key.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# schema = WOWSQL::WOWSQLSchema.new(
|
|
12
|
+
# "myproject",
|
|
13
|
+
# "wowsql_service_..."
|
|
14
|
+
# )
|
|
15
|
+
# schema.create_table("users", [
|
|
16
|
+
# { "name" => "id", "type" => "SERIAL", "auto_increment" => true },
|
|
17
|
+
# { "name" => "email", "type" => "VARCHAR(255)", "unique" => true, "nullable" => false },
|
|
18
|
+
# { "name" => "name", "type" => "VARCHAR(255)" },
|
|
19
|
+
# { "name" => "metadata", "type" => "JSONB", "default" => "'{}'" },
|
|
20
|
+
# { "name" => "created_at", "type" => "TIMESTAMPTZ", "default" => "CURRENT_TIMESTAMP" },
|
|
21
|
+
# ], primary_key: "id", indexes: ["email"])
|
|
22
|
+
class WOWSQLSchema
|
|
23
|
+
attr_reader :base_url
|
|
24
|
+
|
|
25
|
+
# @param project_url [String] Project subdomain or full URL
|
|
26
|
+
# @param service_key [String] Service role key (wowsql_service_...)
|
|
27
|
+
# @param base_domain [String] Base domain (default: "wowsql.com")
|
|
28
|
+
# @param secure [Boolean] Use HTTPS (default: true)
|
|
29
|
+
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
30
|
+
# @param verify_ssl [Boolean] Verify SSL certificates (default: true)
|
|
31
|
+
def initialize(project_url, service_key, base_domain: 'wowsql.com', secure: true,
|
|
32
|
+
timeout: 30, verify_ssl: true)
|
|
33
|
+
if project_url.start_with?('http://') || project_url.start_with?('https://')
|
|
34
|
+
base = project_url.chomp('/')
|
|
35
|
+
base = base.split('/api').first if base.include?('/api')
|
|
36
|
+
@base_url = base
|
|
37
|
+
else
|
|
38
|
+
protocol = secure ? 'https' : 'http'
|
|
39
|
+
if project_url.include?(".#{base_domain}") || project_url.end_with?(base_domain)
|
|
40
|
+
@base_url = "#{protocol}://#{project_url}"
|
|
41
|
+
else
|
|
42
|
+
@base_url = "#{protocol}://#{project_url}.#{base_domain}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@timeout = timeout
|
|
47
|
+
|
|
48
|
+
ssl_options = verify_ssl ? {} : { verify: false }
|
|
49
|
+
|
|
50
|
+
@conn = Faraday.new(url: @base_url, ssl: ssl_options) do |f|
|
|
51
|
+
f.request :json
|
|
52
|
+
f.response :json
|
|
53
|
+
f.adapter Faraday.default_adapter
|
|
54
|
+
f.options.timeout = timeout
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@conn.headers['Authorization'] = "Bearer #{service_key}"
|
|
58
|
+
@conn.headers['Content-Type'] = 'application/json'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# ── Table operations ─────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
# Create a new table.
|
|
64
|
+
#
|
|
65
|
+
# @param table_name [String] Name of the table
|
|
66
|
+
# @param columns [Array<Hash>] Column definitions (name, type, auto_increment, unique, nullable, default)
|
|
67
|
+
# @param primary_key [String, nil] Primary key column name
|
|
68
|
+
# @param indexes [Array<String>, nil] Columns to create indexes on
|
|
69
|
+
# @return [Hash]
|
|
70
|
+
def create_table(table_name, columns, primary_key: nil, indexes: nil)
|
|
71
|
+
request('POST', '/api/v2/schema/tables', nil, {
|
|
72
|
+
table_name: table_name,
|
|
73
|
+
columns: columns,
|
|
74
|
+
primary_key: primary_key,
|
|
75
|
+
indexes: indexes
|
|
76
|
+
})
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Alter an existing table.
|
|
80
|
+
#
|
|
81
|
+
# Operations: add_column, drop_column, modify_column, rename_column
|
|
82
|
+
#
|
|
83
|
+
# @param table_name [String] Table name
|
|
84
|
+
# @param operation [String] Operation type
|
|
85
|
+
# @param column_name [String, nil] Column name
|
|
86
|
+
# @param column_type [String, nil] Column type
|
|
87
|
+
# @param new_column_name [String, nil] New column name (for rename)
|
|
88
|
+
# @param nullable [Boolean] Whether column is nullable
|
|
89
|
+
# @param default [String, nil] Default value
|
|
90
|
+
# @return [Hash]
|
|
91
|
+
def alter_table(table_name, operation, column_name: nil, column_type: nil,
|
|
92
|
+
new_column_name: nil, nullable: true, default: nil)
|
|
93
|
+
request('PATCH', "/api/v2/schema/tables/#{table_name}", nil, {
|
|
94
|
+
table_name: table_name,
|
|
95
|
+
operation: operation,
|
|
96
|
+
column_name: column_name,
|
|
97
|
+
column_type: column_type,
|
|
98
|
+
new_column_name: new_column_name,
|
|
99
|
+
nullable: nullable,
|
|
100
|
+
default: default
|
|
101
|
+
})
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Drop a table. WARNING: This cannot be undone!
|
|
105
|
+
#
|
|
106
|
+
# @param table_name [String] Table to drop
|
|
107
|
+
# @param cascade [Boolean] Also drop dependent objects
|
|
108
|
+
# @return [Hash]
|
|
109
|
+
def drop_table(table_name, cascade: false)
|
|
110
|
+
request('DELETE', "/api/v2/schema/tables/#{table_name}", { 'cascade' => cascade }, nil)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Execute raw DDL SQL.
|
|
114
|
+
#
|
|
115
|
+
# Only schema statements are allowed: CREATE TABLE, ALTER TABLE,
|
|
116
|
+
# DROP TABLE, CREATE INDEX, DROP INDEX, etc.
|
|
117
|
+
#
|
|
118
|
+
# @param sql [String] DDL SQL statement
|
|
119
|
+
# @return [Hash]
|
|
120
|
+
def execute_sql(sql)
|
|
121
|
+
request('POST', '/api/v2/schema/execute', nil, { sql: sql })
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# ── Convenience methods ──────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
# Add a column to an existing table.
|
|
127
|
+
#
|
|
128
|
+
# @param table_name [String] Table name
|
|
129
|
+
# @param column_name [String] Column name
|
|
130
|
+
# @param column_type [String] Column type
|
|
131
|
+
# @param nullable [Boolean] Whether column is nullable
|
|
132
|
+
# @param default [String, nil] Default value
|
|
133
|
+
# @return [Hash]
|
|
134
|
+
def add_column(table_name, column_name, column_type, nullable: true, default: nil)
|
|
135
|
+
alter_table(
|
|
136
|
+
table_name, 'add_column',
|
|
137
|
+
column_name: column_name,
|
|
138
|
+
column_type: column_type,
|
|
139
|
+
nullable: nullable,
|
|
140
|
+
default: default
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Drop a column from a table.
|
|
145
|
+
#
|
|
146
|
+
# @param table_name [String] Table name
|
|
147
|
+
# @param column_name [String] Column name
|
|
148
|
+
# @return [Hash]
|
|
149
|
+
def drop_column(table_name, column_name)
|
|
150
|
+
alter_table(table_name, 'drop_column', column_name: column_name)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Rename a column.
|
|
154
|
+
#
|
|
155
|
+
# @param table_name [String] Table name
|
|
156
|
+
# @param old_name [String] Current column name
|
|
157
|
+
# @param new_name [String] New column name
|
|
158
|
+
# @return [Hash]
|
|
159
|
+
def rename_column(table_name, old_name, new_name)
|
|
160
|
+
alter_table(
|
|
161
|
+
table_name, 'rename_column',
|
|
162
|
+
column_name: old_name,
|
|
163
|
+
new_column_name: new_name
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Change column type, nullability, or default value.
|
|
168
|
+
#
|
|
169
|
+
# @param table_name [String] Table name
|
|
170
|
+
# @param column_name [String] Column name
|
|
171
|
+
# @param column_type [String, nil] New column type
|
|
172
|
+
# @param nullable [Boolean, nil] New nullability
|
|
173
|
+
# @param default [String, nil] New default value
|
|
174
|
+
# @return [Hash]
|
|
175
|
+
def modify_column(table_name, column_name, column_type: nil, nullable: nil, default: nil)
|
|
176
|
+
kwargs = { column_name: column_name }
|
|
177
|
+
kwargs[:column_type] = column_type unless column_type.nil?
|
|
178
|
+
kwargs[:nullable] = nullable unless nullable.nil?
|
|
179
|
+
kwargs[:default] = default unless default.nil?
|
|
180
|
+
alter_table(table_name, 'modify_column', **kwargs)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Create an index.
|
|
184
|
+
#
|
|
185
|
+
# @param table_name [String] Table to index
|
|
186
|
+
# @param columns [String, Array<String>] Column(s)
|
|
187
|
+
# @param unique [Boolean] Create a UNIQUE index
|
|
188
|
+
# @param name [String, nil] Custom index name
|
|
189
|
+
# @param using [String, nil] Index method (btree, hash, gin, gist)
|
|
190
|
+
# @return [Hash]
|
|
191
|
+
def create_index(table_name, columns, unique: false, name: nil, using: nil)
|
|
192
|
+
cols = columns.is_a?(Array) ? columns : [columns]
|
|
193
|
+
idx_name = name || "idx_#{table_name}_#{cols.join('_')}"
|
|
194
|
+
unique_kw = unique ? 'UNIQUE ' : ''
|
|
195
|
+
using_kw = using ? " USING #{using}" : ''
|
|
196
|
+
col_list = cols.map { |c| "\"#{c}\"" }.join(', ')
|
|
197
|
+
sql = "CREATE #{unique_kw}INDEX IF NOT EXISTS \"#{idx_name}\" ON \"#{table_name}\"#{using_kw} (#{col_list})"
|
|
198
|
+
execute_sql(sql)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# List all tables via the v2 REST API.
|
|
202
|
+
#
|
|
203
|
+
# @return [Array<String>]
|
|
204
|
+
def list_tables
|
|
205
|
+
resp = request('GET', '/api/v2/tables', nil, nil)
|
|
206
|
+
resp['tables'] || []
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Get column-level schema information for a table.
|
|
210
|
+
#
|
|
211
|
+
# @param table_name [String] Table name
|
|
212
|
+
# @return [Hash]
|
|
213
|
+
def get_table_schema(table_name)
|
|
214
|
+
request('GET', "/api/v2/tables/#{table_name}/schema", nil, nil)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Close the HTTP connection.
|
|
218
|
+
def close
|
|
219
|
+
@conn.close if @conn.respond_to?(:close)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
def request(method, path, params = nil, json = nil)
|
|
225
|
+
response = @conn.public_send(method.downcase, path) do |req|
|
|
226
|
+
req.params = params if params
|
|
227
|
+
req.body = json if json
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
if response.status == 403
|
|
231
|
+
raise SchemaPermissionError.new(
|
|
232
|
+
'Schema operations require a SERVICE ROLE key. ' \
|
|
233
|
+
'You are using an anonymous key which cannot modify database schema.',
|
|
234
|
+
403
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
if response.status >= 400
|
|
239
|
+
error_data = response.body.is_a?(Hash) ? response.body : {}
|
|
240
|
+
error_msg = error_data['detail'] || error_data['message'] ||
|
|
241
|
+
"Request failed with status #{response.status}"
|
|
242
|
+
raise WOWSQLError.new(error_msg, response.status, error_data)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
response.body
|
|
246
|
+
rescue Faraday::Error => e
|
|
247
|
+
raise WOWSQLError.new("Request failed: #{e.message}")
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|