tina4ruby 3.13.137 → 3.13.138
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 +36 -0
- data/COMMERCIAL-LICENSE.md +8 -0
- data/LICENSE +373 -0
- data/LICENSE.txt +373 -21
- data/NOTICE +11 -0
- data/README.md +11 -7
- data/exe/tina4ruby +6 -0
- data/lib/tina4/ai.rb +6 -0
- data/lib/tina4/ai_client.rb +10 -3
- data/lib/tina4/api.rb +57 -7
- data/lib/tina4/auth.rb +18 -12
- data/lib/tina4/auto_crud.rb +83 -27
- data/lib/tina4/background.rb +7 -1
- data/lib/tina4/base64.rb +59 -0
- data/lib/tina4/cache.rb +6 -0
- data/lib/tina4/cache_backends/base_backend.rb +6 -0
- data/lib/tina4/cache_backends/database_backend.rb +6 -0
- data/lib/tina4/cache_backends/file_backend.rb +6 -0
- data/lib/tina4/cache_backends/memcached_backend.rb +6 -0
- data/lib/tina4/cache_backends/memory_backend.rb +6 -0
- data/lib/tina4/cache_backends/mongo_backend.rb +6 -0
- data/lib/tina4/cache_backends/redis_backend.rb +6 -0
- data/lib/tina4/cache_backends/valkey_backend.rb +6 -0
- data/lib/tina4/cache_backends.rb +6 -0
- data/lib/tina4/cli.rb +22 -7
- data/lib/tina4/constants.rb +12 -8
- data/lib/tina4/container.rb +6 -0
- data/lib/tina4/context/chunker.rb +6 -0
- data/lib/tina4/context.rb +13 -5
- data/lib/tina4/cors.rb +66 -10
- data/lib/tina4/crud.rb +78 -9
- data/lib/tina4/database/sqlite3_adapter.rb +9 -1
- data/lib/tina4/database.rb +260 -44
- data/lib/tina4/database_adapter.rb +6 -0
- data/lib/tina4/database_result.rb +6 -0
- data/lib/tina4/database_url.rb +6 -0
- data/lib/tina4/debug.rb +6 -0
- data/lib/tina4/dev.rb +6 -0
- data/lib/tina4/dev_admin.rb +215 -89
- data/lib/tina4/dev_mailbox.rb +6 -1
- data/lib/tina4/dispatch_pipeline.rb +86 -1
- data/lib/tina4/docs.rb +6 -0
- data/lib/tina4/docstore.rb +23 -4
- data/lib/tina4/drivers/arango_graph_driver.rb +8 -1
- data/lib/tina4/drivers/bolt_graph_driver.rb +6 -0
- data/lib/tina4/drivers/firebird_driver.rb +95 -6
- data/lib/tina4/drivers/mongodb_driver.rb +6 -0
- data/lib/tina4/drivers/mssql_driver.rb +54 -39
- data/lib/tina4/drivers/mysql_driver.rb +6 -0
- data/lib/tina4/drivers/odbc_driver.rb +6 -0
- data/lib/tina4/drivers/postgres_driver.rb +35 -8
- data/lib/tina4/drivers/schema_split.rb +6 -0
- data/lib/tina4/drivers/sqlite_driver.rb +8 -1
- data/lib/tina4/drivers/ultipa_graph_driver.rb +6 -0
- data/lib/tina4/env.rb +6 -0
- data/lib/tina4/error_overlay.rb +6 -0
- data/lib/tina4/events.rb +6 -0
- data/lib/tina4/feedback.rb +9 -2
- data/lib/tina4/field_types.rb +6 -0
- data/lib/tina4/form_parser.rb +241 -0
- data/lib/tina4/frond.rb +196 -56
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +6 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +6 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +6 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +6 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +6 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +6 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +6 -0
- data/lib/tina4/graph.rb +6 -0
- data/lib/tina4/graphql.rb +28 -27
- data/lib/tina4/health.rb +6 -0
- data/lib/tina4/html_element.rb +6 -0
- data/lib/tina4/http_server.rb +690 -0
- data/lib/tina4/imap_client.rb +352 -0
- data/lib/tina4/import_helper.rb +6 -0
- data/lib/tina4/job.rb +6 -0
- data/lib/tina4/localization.rb +6 -0
- data/lib/tina4/log.rb +6 -0
- data/lib/tina4/mail_socket.rb +124 -0
- data/lib/tina4/mcp.rb +15 -8
- data/lib/tina4/messenger.rb +86 -55
- data/lib/tina4/metrics.rb +6 -0
- data/lib/tina4/middleware.rb +24 -9
- data/lib/tina4/migration.rb +6 -0
- data/lib/tina4/model_collection.rb +6 -0
- data/lib/tina4/mqtt.rb +9 -2
- data/lib/tina4/mqtt_message.rb +6 -0
- data/lib/tina4/orm.rb +72 -16
- data/lib/tina4/parse_json.rb +25 -0
- data/lib/tina4/plan.rb +9 -2
- data/lib/tina4/point.rb +6 -0
- data/lib/tina4/port_takeover.rb +6 -0
- data/lib/tina4/project_index.rb +6 -0
- data/lib/tina4/public/THIRD-PARTY-NOTICES.md +55 -0
- data/lib/tina4/public/bundled-component-provenance.json +48 -0
- data/lib/tina4/public/js/tina4js.min.js +13 -3
- data/lib/tina4/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
- data/lib/tina4/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
- data/lib/tina4/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
- data/lib/tina4/public/licenses/tina4-css-MIT.txt +21 -0
- data/lib/tina4/public/licenses/tina4-dev-admin-components.json +240 -0
- data/lib/tina4/public/licenses/tina4-dev-admin-third-party.txt +919 -0
- data/lib/tina4/public/licenses/tina4-js-MIT.txt +20 -0
- data/lib/tina4/push.rb +9 -3
- data/lib/tina4/query_builder.rb +6 -0
- data/lib/tina4/queue.rb +6 -0
- data/lib/tina4/queue_backends/kafka_backend.rb +10 -2
- data/lib/tina4/queue_backends/lite_backend.rb +6 -0
- data/lib/tina4/queue_backends/mongo_backend.rb +6 -0
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +10 -2
- data/lib/tina4/rack_app.rb +11 -1
- data/lib/tina4/rate_limiter.rb +6 -0
- data/lib/tina4/realtime/attachment.rb +6 -0
- data/lib/tina4/realtime/channel.rb +6 -0
- data/lib/tina4/realtime/channel_member.rb +6 -0
- data/lib/tina4/realtime/local_storage.rb +6 -0
- data/lib/tina4/realtime/message.rb +6 -0
- data/lib/tina4/realtime/s3_storage.rb +6 -0
- data/lib/tina4/realtime/storage.rb +6 -0
- data/lib/tina4/realtime/storage_backend.rb +6 -0
- data/lib/tina4/realtime/workspace.rb +6 -0
- data/lib/tina4/realtime.rb +10 -3
- data/lib/tina4/request.rb +85 -127
- data/lib/tina4/response.rb +71 -9
- data/lib/tina4/response_cache.rb +6 -0
- data/lib/tina4/router.rb +6 -0
- data/lib/tina4/secret_file.rb +34 -0
- data/lib/tina4/seeder.rb +9 -3
- data/lib/tina4/service.rb +8 -2
- data/lib/tina4/service_runner.rb +6 -0
- data/lib/tina4/session.rb +6 -0
- data/lib/tina4/session_handlers/database_handler.rb +6 -0
- data/lib/tina4/session_handlers/file_handler.rb +6 -0
- data/lib/tina4/session_handlers/memcached_handler.rb +6 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +108 -39
- data/lib/tina4/session_handlers/mongo_wire_client.rb +23 -9
- data/lib/tina4/session_handlers/redis_handler.rb +6 -0
- data/lib/tina4/session_handlers/resp_client.rb +6 -0
- data/lib/tina4/session_handlers/valkey_handler.rb +6 -0
- data/lib/tina4/shutdown.rb +17 -14
- data/lib/tina4/smtp_client.rb +222 -0
- data/lib/tina4/sql_translator.rb +93 -0
- data/lib/tina4/sqlite3_gem.rb +27 -0
- data/lib/tina4/sso.rb +25 -8
- data/lib/tina4/swagger.rb +6 -0
- data/lib/tina4/template.rb +6 -0
- data/lib/tina4/test.rb +8 -2
- data/lib/tina4/test_client.rb +8 -1
- data/lib/tina4/testing.rb +8 -1
- data/lib/tina4/validator.rb +6 -0
- data/lib/tina4/version.rb +7 -1
- data/lib/tina4/webserver.rb +25 -143
- data/lib/tina4/websocket.rb +10 -4
- data/lib/tina4/websocket_backplane.rb +19 -2
- data/lib/tina4/wsdl.rb +31 -16
- data/lib/tina4/xml_parser.rb +259 -0
- data/lib/tina4.rb +87 -31
- data/lib/tina4ruby.rb +6 -0
- metadata +43 -140
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "zlib"
|
|
4
10
|
require "stringio"
|
|
@@ -77,13 +83,24 @@ module Tina4
|
|
|
77
83
|
# Content-Length reflects the (possibly compressed) body the equivalent
|
|
78
84
|
# GET would have sent, mirroring the Python master's build_headers() ->
|
|
79
85
|
# app() ordering (compress -> tag -> 304-decide -> strip-for-HEAD).
|
|
86
|
+
#
|
|
87
|
+
# `security_headers` is here for the same reason: a refusal (a CSRF 403, a
|
|
88
|
+
# legacy auth_handler 403, a pre-match middleware's 429), a 404 and a
|
|
89
|
+
# static file are all produced OUTSIDE the post-match middleware pass that
|
|
90
|
+
# runs SecurityHeadersMiddleware, so they left without CSP, nosniff and
|
|
91
|
+
# X-Frame-Options (python #137, php CSRF-403 parity).
|
|
80
92
|
ALWAYS_STAGES = %i[
|
|
81
93
|
compress_and_tag
|
|
82
94
|
conditional_get
|
|
83
95
|
head_strip
|
|
84
96
|
apply_cors
|
|
97
|
+
security_headers
|
|
85
98
|
].freeze
|
|
86
99
|
|
|
100
|
+
# The three headers whose presence shows SecurityHeadersMiddleware already
|
|
101
|
+
# ran for this response (lower-case: how the pipeline stores them).
|
|
102
|
+
SECURITY_HEADER_PROBE = %w[content-security-policy x-content-type-options x-frame-options].freeze
|
|
103
|
+
|
|
87
104
|
RESPONSE_STAGES = %i[
|
|
88
105
|
dev_inspector_capture
|
|
89
106
|
request_log
|
|
@@ -130,7 +147,7 @@ module Tina4
|
|
|
130
147
|
DispatchContext = Struct.new(
|
|
131
148
|
:env, :method, :path, :started_at,
|
|
132
149
|
:pre_request, :pre_response, :matched_pattern, :matched,
|
|
133
|
-
:bypass_response_stages,
|
|
150
|
+
:bypass_response_stages, :security_exempt,
|
|
134
151
|
keyword_init: true
|
|
135
152
|
)
|
|
136
153
|
|
|
@@ -172,11 +189,22 @@ module Tina4
|
|
|
172
189
|
# WebSocket upgrade - match against registered ws_routes.
|
|
173
190
|
def websocket_upgrade(ctx)
|
|
174
191
|
return nil unless websocket_upgrade?(ctx.env)
|
|
192
|
+
# The AI/test port never holds a live-reload socket: fall through to
|
|
193
|
+
# dev_routes, which answers "Not available on AI port" (Python's built-in
|
|
194
|
+
# server refuses the same upgrade with 404).
|
|
195
|
+
return nil if ctx.path == "/__dev_reload" && ctx.env["tina4.ai_port"]
|
|
196
|
+
|
|
197
|
+
# ADR-0082: the dev reload socket answers only a loopback / TINA4_HOST
|
|
198
|
+
# Host - the same rule the /__dev HTTP gate applies (DNS rebinding).
|
|
199
|
+
if ctx.path.start_with?("/__dev") && !Tina4::DevAdmin.dev_host_allowed?(ctx.env)
|
|
200
|
+
return [403, { "content-type" => "text/plain" }, ["Forbidden: host not allowed"]]
|
|
201
|
+
end
|
|
175
202
|
|
|
176
203
|
ws_result = Tina4::Router.find_ws_route(ctx.path)
|
|
177
204
|
return nil unless ws_result
|
|
178
205
|
|
|
179
206
|
ws_route, ws_params = ws_result
|
|
207
|
+
ctx.security_exempt = true # a protocol switch, not a document
|
|
180
208
|
handle_websocket_upgrade(ctx.env, ws_route, ws_params)
|
|
181
209
|
end
|
|
182
210
|
|
|
@@ -190,6 +218,9 @@ module Tina4
|
|
|
190
218
|
return [404, { "content-type" => "text/plain" }, ["Not available on AI port"]]
|
|
191
219
|
end
|
|
192
220
|
|
|
221
|
+
# The dev dashboard is a debug-only surface with its own inline scripts;
|
|
222
|
+
# it keeps its historical headers.
|
|
223
|
+
ctx.security_exempt = true
|
|
193
224
|
Tina4::DevAdmin.handle_request(ctx.env)
|
|
194
225
|
end
|
|
195
226
|
|
|
@@ -284,11 +315,15 @@ module Tina4
|
|
|
284
315
|
# 9110 s9.3.2. Recorded as a finding and fixed separately with its own test
|
|
285
316
|
# pair - NOT silently inside this extraction.
|
|
286
317
|
def not_found(ctx)
|
|
318
|
+
# The swagger UI loads its assets from TINA4_SWAGGER_UI_CDN, which the
|
|
319
|
+
# default CSP (default-src 'self') would block, so it stays exempt.
|
|
287
320
|
if ctx.path == "/swagger" || ctx.path == "/swagger/"
|
|
288
321
|
ctx.bypass_response_stages = true
|
|
322
|
+
ctx.security_exempt = true
|
|
289
323
|
return serve_swagger_ui
|
|
290
324
|
elsif ctx.path == "/swagger/openapi.json"
|
|
291
325
|
ctx.bypass_response_stages = true
|
|
326
|
+
ctx.security_exempt = true
|
|
292
327
|
return serve_openapi_json
|
|
293
328
|
end
|
|
294
329
|
|
|
@@ -301,6 +336,56 @@ module Tina4
|
|
|
301
336
|
handle_404(ctx.path, ctx.env["HTTP_ACCEPT"])
|
|
302
337
|
end
|
|
303
338
|
|
|
339
|
+
# ALWAYS stage: every response gets the security headers a route response
|
|
340
|
+
# gets, whichever stage produced it. See ALWAYS_STAGES.
|
|
341
|
+
def security_headers(ctx, response)
|
|
342
|
+
return nil if ctx.security_exempt
|
|
343
|
+
|
|
344
|
+
with_security_headers(ctx, response)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Issue #137 (parity with tina4-python #137): a static file gets the SAME
|
|
348
|
+
# security headers a route response gets.
|
|
349
|
+
#
|
|
350
|
+
# SecurityHeadersMiddleware is post-match global middleware, so it only ever
|
|
351
|
+
# ran for a MATCHED route; a file from public/ or src/public/ answers from
|
|
352
|
+
# this not-found fallback and skipped it. The same HTML carried CSP, nosniff
|
|
353
|
+
# and X-Frame-Options from a route and none of them as a file - and since
|
|
354
|
+
# "/" resolves to index.html, an SPA's front door was frameable
|
|
355
|
+
# (clickjacking) and ran without a CSP.
|
|
356
|
+
#
|
|
357
|
+
# Only this middleware is applied, and only when it is ATTACHED (which
|
|
358
|
+
# Tina4.initialize! does by default), so removing it still opts a static
|
|
359
|
+
# file out too. The headers come from the middleware's own before hook, so
|
|
360
|
+
# the TINA4_CSP / TINA4_FRAME_OPTIONS / TINA4_HSTS rules stay in one place.
|
|
361
|
+
# Other global middleware (auth, CSRF, rate limits) is deliberately NOT run
|
|
362
|
+
# here: static serving has never been subject to it. A header the file
|
|
363
|
+
# response already set is never overwritten.
|
|
364
|
+
def with_security_headers(ctx, triple)
|
|
365
|
+
return nil unless Tina4::Middleware.global_middleware.include?(Tina4::SecurityHeadersMiddleware)
|
|
366
|
+
|
|
367
|
+
status, headers, body = triple
|
|
368
|
+
# Header names are compared case-insensitively: a route response carries
|
|
369
|
+
# them as the middleware spelled them ("X-Frame-Options").
|
|
370
|
+
present = headers.keys.map { |name| name.to_s.downcase }
|
|
371
|
+
# A matched route already ran the middleware's hook.
|
|
372
|
+
return nil if SECURITY_HEADER_PROBE.all? { |name| present.include?(name) }
|
|
373
|
+
|
|
374
|
+
probe = Tina4::Response.new
|
|
375
|
+
defaults = probe.headers.keys
|
|
376
|
+
request = ctx.pre_request || Tina4::Request.new(ctx.env)
|
|
377
|
+
Tina4::SecurityHeadersMiddleware.before_security(request, probe)
|
|
378
|
+
|
|
379
|
+
merged = headers.dup
|
|
380
|
+
probe.headers.each do |name, value|
|
|
381
|
+
next if defaults.include?(name)
|
|
382
|
+
|
|
383
|
+
key = name.to_s.downcase
|
|
384
|
+
merged[key] = value unless present.include?(key)
|
|
385
|
+
end
|
|
386
|
+
[status, merged, body]
|
|
387
|
+
end
|
|
388
|
+
|
|
304
389
|
# ── ALWAYS STAGES (compression / ETag / conditional-GET) ──────────
|
|
305
390
|
# Each returns a replacement Rack triple, or nil to leave it unchanged.
|
|
306
391
|
|
data/lib/tina4/docs.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
# Tina4::Docs — Live API RAG.
|
|
4
10
|
#
|
data/lib/tina4/docstore.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
# Tina4 DocStore - pymongo-style document storage with a zero-config SQLite fallback.
|
|
4
10
|
#
|
|
@@ -47,6 +53,7 @@ require "json"
|
|
|
47
53
|
require "time"
|
|
48
54
|
require "securerandom"
|
|
49
55
|
require "delegate"
|
|
56
|
+
require_relative "sqlite3_gem"
|
|
50
57
|
|
|
51
58
|
module Tina4
|
|
52
59
|
module DocStore
|
|
@@ -190,12 +197,24 @@ module Tina4
|
|
|
190
197
|
|
|
191
198
|
COMPARATORS = { "$gt" => ">", "$gte" => ">=", "$lt" => "<", "$lte" => "<=" }.freeze
|
|
192
199
|
|
|
200
|
+
FIELD_SEGMENT_RE = /\A[A-Za-z0-9_-]+\z/.freeze
|
|
201
|
+
|
|
193
202
|
# Field name -> a JSON path. Dotted names address nested keys.
|
|
203
|
+
#
|
|
204
|
+
# tina4: ADR-0069 - this is the single chokepoint every field path passes
|
|
205
|
+
# through on its way into SQL (extract / json_type / json_each, so filters
|
|
206
|
+
# at any depth, operator fields and sort keys). Every dot-separated segment
|
|
207
|
+
# must match [A-Za-z0-9_-]+ or the path is rejected before any SQL is built.
|
|
194
208
|
def json_path(field)
|
|
195
|
-
|
|
196
|
-
|
|
209
|
+
path = field.to_s
|
|
210
|
+
segments = path.split(".", -1)
|
|
211
|
+
if segments.empty? || !segments.all? { |segment| segment.match?(FIELD_SEGMENT_RE) }
|
|
212
|
+
raise ArgumentError,
|
|
213
|
+
"DocStore: invalid field path '#{path}' - each dot-separated segment must match [A-Za-z0-9_-]+"
|
|
197
214
|
end
|
|
198
|
-
|
|
215
|
+
|
|
216
|
+
quoted = segments.map { |segment| segment.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/) ? segment : "\"#{segment}\"" }
|
|
217
|
+
"$." + quoted.join(".")
|
|
199
218
|
end
|
|
200
219
|
|
|
201
220
|
def extract(field) = "json_extract(doc, '#{json_path(field)}')"
|
|
@@ -813,7 +832,7 @@ module Tina4
|
|
|
813
832
|
|
|
814
833
|
def initialize(path = nil)
|
|
815
834
|
@path = path || ENV["TINA4_DOC_STORE_PATH"] || "data/tina4_docstore.db"
|
|
816
|
-
|
|
835
|
+
Tina4.require_sqlite3!
|
|
817
836
|
if @path != ":memory:"
|
|
818
837
|
dir = File.dirname(@path)
|
|
819
838
|
require "fileutils"
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
# ArangoDB graph adapter — the document/AQL engine behind the same surface.
|
|
4
10
|
#
|
|
@@ -18,6 +24,7 @@
|
|
|
18
24
|
require "net/http"
|
|
19
25
|
require "json"
|
|
20
26
|
require "uri"
|
|
27
|
+
require_relative "../parse_json"
|
|
21
28
|
|
|
22
29
|
module Tina4
|
|
23
30
|
module Drivers
|
|
@@ -221,7 +228,7 @@ module Tina4
|
|
|
221
228
|
end
|
|
222
229
|
|
|
223
230
|
def parse_response(response)
|
|
224
|
-
parsed = response.body && !response.body.empty? ?
|
|
231
|
+
parsed = response.body && !response.body.empty? ? Tina4.parse_json(response.body) : {}
|
|
225
232
|
code = response.code.to_i
|
|
226
233
|
if code >= 400 || parsed["error"] == true
|
|
227
234
|
message = parsed["errorMessage"] || parsed["error"] || "HTTP #{response.code}"
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
# Bolt graph adapter — Neo4j AND Memgraph (both speak Bolt + Cypher).
|
|
4
10
|
#
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
require "set"
|
|
2
8
|
# frozen_string_literal: true
|
|
3
9
|
|
|
@@ -7,6 +13,15 @@ module Tina4
|
|
|
7
13
|
include Tina4::DatabaseAdapter
|
|
8
14
|
attr_reader :connection
|
|
9
15
|
|
|
16
|
+
# Raised when a statement that returns values from a WRITE
|
|
17
|
+
# (INSERT/UPDATE/DELETE ... RETURNING, or EXECUTE PROCEDURE with outputs)
|
|
18
|
+
# is read through fetch/fetch_one. See #execute_query.
|
|
19
|
+
class ReturningNotSupported < StandardError; end
|
|
20
|
+
|
|
21
|
+
# The `fb` gem's error for a fetch from a cursor Firebird never opened
|
|
22
|
+
# (SQLCODE -504) - what a singleton RETURNING result produces.
|
|
23
|
+
CURSOR_NOT_OPEN_MARKERS = ["cursor is not open", "unknown cursor"].freeze
|
|
24
|
+
|
|
10
25
|
# Substring markers (lowercased) that identify a dead-socket Firebird
|
|
11
26
|
# error worth reconnecting for. Idle Firebird connections die silently
|
|
12
27
|
# behind NAT timeouts, server-side ConnectionIdleTimeout, or Docker
|
|
@@ -219,14 +234,45 @@ module Tina4
|
|
|
219
234
|
@connection&.close
|
|
220
235
|
end
|
|
221
236
|
|
|
237
|
+
# Run a statement that returns rows and hydrate them.
|
|
238
|
+
#
|
|
239
|
+
# The cursor is driven HERE rather than through Connection#query so that a
|
|
240
|
+
# failed fetch can be cleaned up. Connection#query (and #execute) start an
|
|
241
|
+
# AUTOMATIC transaction when none is open and end it only when the cursor
|
|
242
|
+
# is closed after a SUCCESSFUL fetch - and closing it COMMITS. A fetch that
|
|
243
|
+
# raised used to skip all of that: the automatic transaction, holding any
|
|
244
|
+
# write the statement made, stayed open on the connection, every later
|
|
245
|
+
# statement ran inside it, and DDL on the table blocked ("object in use").
|
|
246
|
+
#
|
|
247
|
+
# The one statement that always fails that way is a write that returns
|
|
248
|
+
# values - INSERT/UPDATE/DELETE ... RETURNING. Firebird runs it as an
|
|
249
|
+
# EXECUTE PROCEDURE-type statement whose values come back only through
|
|
250
|
+
# isc_dsql_execute2's OUTPUT sqlda, which the `fb` gem never passes: it
|
|
251
|
+
# opens no cursor and the first fetch raises "Cursor is not open" (-504).
|
|
252
|
+
# There is no other call in the gem that returns them (measured, fb 0.10.0
|
|
253
|
+
# on Firebird 5), so it is refused with a clear ReturningNotSupported.
|
|
254
|
+
#
|
|
255
|
+
# On ANY fetch failure: outside an explicit transaction the automatic one
|
|
256
|
+
# is ROLLED BACK first (nothing is written, nothing stays locked, and the
|
|
257
|
+
# cursor's close can no longer commit it); inside an explicit transaction
|
|
258
|
+
# it is left for the caller's own rollback. The cursor is then dropped so
|
|
259
|
+
# its prepared statement does not hold the table.
|
|
222
260
|
def execute_query(sql, params = [])
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
261
|
+
cursor = with_reconnect do
|
|
262
|
+
params.empty? ? @connection.execute(sql) : @connection.execute(sql, *params)
|
|
263
|
+
end
|
|
264
|
+
# A statement with no result set: execute already ran (and, outside a
|
|
265
|
+
# transaction, committed) it and returned a count.
|
|
266
|
+
return [] unless cursor.respond_to?(:fetchall)
|
|
267
|
+
|
|
268
|
+
rows = begin
|
|
269
|
+
cursor.fetchall(:hash)
|
|
270
|
+
rescue StandardError => e
|
|
271
|
+
abandon_cursor(cursor)
|
|
272
|
+
raise returning_not_supported if cursor_not_open?(e)
|
|
273
|
+
raise
|
|
229
274
|
end
|
|
275
|
+
cursor.close
|
|
230
276
|
rows.map { |row| decode_blobs(symbolize_keys(row)) }
|
|
231
277
|
end
|
|
232
278
|
|
|
@@ -438,6 +484,49 @@ module Tina4
|
|
|
438
484
|
yield
|
|
439
485
|
end
|
|
440
486
|
|
|
487
|
+
# Clean up after a fetch that raised: roll back the AUTOMATIC transaction
|
|
488
|
+
# (never an explicit one - that is the caller's) BEFORE the cursor is
|
|
489
|
+
# released, then free the cursor's prepared statement.
|
|
490
|
+
#
|
|
491
|
+
# Cursor#close, not #drop: #drop raises on the DSQL_close of a cursor
|
|
492
|
+
# Firebird never opened and so never reaches DSQL_drop, leaving the
|
|
493
|
+
# statement - and the table - held until GC ("object in use" on DDL).
|
|
494
|
+
# #close only warns on that step and always drops. It commits only the
|
|
495
|
+
# cursor's OWN automatic transaction, which is already rolled back here
|
|
496
|
+
# (the handle no longer matches), and never an explicit one.
|
|
497
|
+
def abandon_cursor(cursor)
|
|
498
|
+
begin
|
|
499
|
+
@connection.rollback if !@in_transaction && @connection.transaction_started
|
|
500
|
+
rescue StandardError
|
|
501
|
+
nil # the original error is the one worth reporting
|
|
502
|
+
end
|
|
503
|
+
begin
|
|
504
|
+
cursor.close
|
|
505
|
+
rescue StandardError
|
|
506
|
+
nil
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def cursor_not_open?(error)
|
|
511
|
+
message = error.message.to_s.downcase
|
|
512
|
+
CURSOR_NOT_OPEN_MARKERS.any? { |marker| message.include?(marker) }
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def returning_not_supported
|
|
516
|
+
outcome = if @in_transaction
|
|
517
|
+
"It ran inside your explicit transaction, which is still open: roll it back."
|
|
518
|
+
else
|
|
519
|
+
"The statement was rolled back: nothing was written and no transaction is left open."
|
|
520
|
+
end
|
|
521
|
+
ReturningNotSupported.new(
|
|
522
|
+
"Firebird: a write that returns values (INSERT/UPDATE/DELETE ... RETURNING, or " \
|
|
523
|
+
"EXECUTE PROCEDURE with outputs) cannot be read back through the `fb` gem - it never " \
|
|
524
|
+
"opens a cursor for a singleton result. #{outcome} Run the write without RETURNING " \
|
|
525
|
+
"and read the row back with a SELECT, use db.insert(...), or write it as " \
|
|
526
|
+
"INSERT ... SELECT ... RETURNING, which Firebird returns as a real cursor."
|
|
527
|
+
)
|
|
528
|
+
end
|
|
529
|
+
|
|
441
530
|
# Hydrate a raw fb-gem row into a SYMBOL-keyed Hash — parity with the
|
|
442
531
|
# sqlite/postgres/mssql drivers (SqliteDriver#symbolize_rows,
|
|
443
532
|
# PostgresDriver#symbolize_result, MssqlDriver's `symbolize_keys: true`),
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Drivers
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require_relative "schema_split"
|
|
4
10
|
|
|
@@ -225,49 +231,58 @@ module Tina4
|
|
|
225
231
|
end
|
|
226
232
|
end
|
|
227
233
|
|
|
234
|
+
# tiny_tds has no parameter binding, so each PLACEHOLDER `?` is replaced by
|
|
235
|
+
# its escaped value in ONE pass over the SQL (python #138). This used to
|
|
236
|
+
# `sub` the first `?` once per param, so a `?` inside a string literal, a
|
|
237
|
+
# [bracketed] or "quoted" identifier or a comment took a value, and so did
|
|
238
|
+
# a `?` inside a value spliced in by the previous param ("a?b" then 2 gave
|
|
239
|
+
# 'a2b' and left a placeholder behind). A `?` beyond the params is left as
|
|
240
|
+
# it was.
|
|
228
241
|
def interpolate_params(sql, params)
|
|
229
242
|
return sql if params.empty?
|
|
230
|
-
result = sql.dup
|
|
231
|
-
params.each do |param|
|
|
232
|
-
escaped =
|
|
233
|
-
if param.nil?
|
|
234
|
-
"NULL"
|
|
235
|
-
elsif param == true
|
|
236
|
-
# SQL Server has no boolean literal — BIT stores 0/1. A raw `true`
|
|
237
|
-
# would interpolate as the bareword `true` ("Invalid column name
|
|
238
|
-
# 'true'"). Coerce at the bind boundary, parity with the SQLite
|
|
239
|
-
# driver's coerce_params and the Python/PHP/Node bind contract.
|
|
240
|
-
"1"
|
|
241
|
-
elsif param == false
|
|
242
|
-
"0"
|
|
243
|
-
elsif param.is_a?(Time) || param.is_a?(DateTime)
|
|
244
|
-
"'#{(param.respond_to?(:iso8601) ? param.iso8601 : param.to_s).gsub("'", "''")}'"
|
|
245
|
-
elsif param.is_a?(String)
|
|
246
|
-
interpolate_string(param)
|
|
247
|
-
elsif param.is_a?(Integer)
|
|
248
|
-
param.to_s
|
|
249
|
-
elsif param.is_a?(Float)
|
|
250
|
-
# A finite Float is a valid numeric literal; NaN/Infinity are not
|
|
251
|
-
# representable in T-SQL and would stringify to a bareword.
|
|
252
|
-
raise ArgumentError, "MssqlDriver cannot bind a non-finite Float (#{param})" unless param.finite?
|
|
253
243
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
244
|
+
Tina4::SQLTranslator.replace_placeholders(sql, brackets: true) do |index|
|
|
245
|
+
index < params.length ? sql_literal(params[index]) : "?"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# One parameter as a T-SQL literal.
|
|
250
|
+
def sql_literal(param)
|
|
251
|
+
if param.nil?
|
|
252
|
+
"NULL"
|
|
253
|
+
elsif param == true
|
|
254
|
+
# SQL Server has no boolean literal — BIT stores 0/1. A raw `true`
|
|
255
|
+
# would interpolate as the bareword `true` ("Invalid column name
|
|
256
|
+
# 'true'"). Coerce at the bind boundary, parity with the SQLite
|
|
257
|
+
# driver's coerce_params and the Python/PHP/Node bind contract.
|
|
258
|
+
"1"
|
|
259
|
+
elsif param == false
|
|
260
|
+
"0"
|
|
261
|
+
elsif param.is_a?(Time) || param.is_a?(DateTime)
|
|
262
|
+
"'#{(param.respond_to?(:iso8601) ? param.iso8601 : param.to_s).gsub("'", "''")}'"
|
|
263
|
+
elsif param.is_a?(String)
|
|
264
|
+
interpolate_string(param)
|
|
265
|
+
elsif param.is_a?(Integer)
|
|
266
|
+
param.to_s
|
|
267
|
+
elsif param.is_a?(Float)
|
|
268
|
+
# A finite Float is a valid numeric literal; NaN/Infinity are not
|
|
269
|
+
# representable in T-SQL and would stringify to a bareword.
|
|
270
|
+
raise ArgumentError, "MssqlDriver cannot bind a non-finite Float (#{param})" unless param.finite?
|
|
271
|
+
|
|
272
|
+
param.to_s
|
|
273
|
+
elsif param.is_a?(Numeric)
|
|
274
|
+
# BigDecimal / Rational -> a plain decimal literal.
|
|
275
|
+
param.to_s
|
|
276
|
+
else
|
|
277
|
+
# MSSQL-INTERP-RUBY: the old `else param.to_s` emitted a BAREWORD for
|
|
278
|
+
# any unrecognised type (a Symbol became `WHERE x = active`, invalid
|
|
279
|
+
# or unintended SQL - a breakage / injection vector). Refuse it loudly
|
|
280
|
+
# instead of splicing an arbitrary object's #to_s into the statement.
|
|
281
|
+
raise ArgumentError,
|
|
282
|
+
"MssqlDriver cannot safely bind a #{param.class} parameter to MSSQL " \
|
|
283
|
+
"(#{param.inspect}); pass nil, true/false, a Time, a String " \
|
|
284
|
+
"(text, or ASCII-8BIT bytes), or a Numeric - never a bareword"
|
|
269
285
|
end
|
|
270
|
-
result
|
|
271
286
|
end
|
|
272
287
|
|
|
273
288
|
# Bind a String parameter. ASCII-8BIT (binary) bytes become a T-SQL
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require_relative "schema_split"
|
|
4
10
|
require_relative "../sql_translator"
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Drivers
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require_relative "schema_split"
|
|
4
10
|
|
|
@@ -59,11 +65,10 @@ module Tina4
|
|
|
59
65
|
end
|
|
60
66
|
|
|
61
67
|
def execute_query(sql, params = [])
|
|
62
|
-
converted_sql = convert_placeholders(sql)
|
|
63
68
|
result = if params.empty?
|
|
64
|
-
@connection.exec(
|
|
69
|
+
@connection.exec(sql) # no params: sent exactly as written
|
|
65
70
|
else
|
|
66
|
-
@connection.exec_params(
|
|
71
|
+
@connection.exec_params(convert_placeholders(sql), params)
|
|
67
72
|
end
|
|
68
73
|
track_affected(result)
|
|
69
74
|
symbolize_result(result)
|
|
@@ -77,11 +82,10 @@ module Tina4
|
|
|
77
82
|
# Clear the cache so last_insert_id falls back to the lastval() probe,
|
|
78
83
|
# which is the correct source for a sequence-backed bare INSERT.
|
|
79
84
|
@last_returning_id = nil if sql.lstrip[0, 6].upcase == "INSERT"
|
|
80
|
-
converted_sql = convert_placeholders(sql)
|
|
81
85
|
result = if params.empty?
|
|
82
|
-
@connection.exec(
|
|
86
|
+
@connection.exec(sql) # no params: sent exactly as written
|
|
83
87
|
else
|
|
84
|
-
@connection.exec_params(
|
|
88
|
+
@connection.exec_params(convert_placeholders(sql), params)
|
|
85
89
|
end
|
|
86
90
|
track_affected(result)
|
|
87
91
|
result
|
|
@@ -127,6 +131,7 @@ module Tina4
|
|
|
127
131
|
result = execute_query(sql, data.values)
|
|
128
132
|
row = result.is_a?(Array) ? result.first : nil
|
|
129
133
|
id = normalize_returned_id(row)
|
|
134
|
+
id = sequence_value_in(row) if id.nil? && row.is_a?(Hash)
|
|
130
135
|
# Remember the real id so a follow-up #last_insert_id / db.get_last_id
|
|
131
136
|
# surfaces THIS value (incl. a UUID string) instead of re-probing
|
|
132
137
|
# lastval(), which has no sequence for a UUID PK and would return a
|
|
@@ -326,6 +331,18 @@ module Tina4
|
|
|
326
331
|
end
|
|
327
332
|
end
|
|
328
333
|
|
|
334
|
+
# A key column not named ``id`` (``person_id SERIAL``) is invisible to
|
|
335
|
+
# row_id_value, so ask the sequence. lastval() is session-wide: on a table
|
|
336
|
+
# with no sequence it is a value from some EARLIER insert, so it is only
|
|
337
|
+
# trusted when the row this INSERT returned actually holds it.
|
|
338
|
+
def sequence_value_in(row)
|
|
339
|
+
@last_returning_id = nil
|
|
340
|
+
value = last_insert_id
|
|
341
|
+
return nil if value.nil?
|
|
342
|
+
|
|
343
|
+
row.values.any? { |column_value| column_value.to_s == value.to_s } ? value : nil
|
|
344
|
+
end
|
|
345
|
+
|
|
329
346
|
# Read the ``id`` column from a RETURNING row regardless of key style
|
|
330
347
|
# (symbol/string, any case) — execute_query symbolizes keys, but stay
|
|
331
348
|
# tolerant.
|
|
@@ -384,9 +401,19 @@ module Tina4
|
|
|
384
401
|
end
|
|
385
402
|
end
|
|
386
403
|
|
|
404
|
+
# ? -> $1, $2 ... for every PLACEHOLDER only: a `?` inside a string
|
|
405
|
+
# literal (incl. E'...' and $$...$$), a quoted identifier or a comment is
|
|
406
|
+
# left alone (python #138). This was a plain gsub, so
|
|
407
|
+
# SELECT 'why?', ? became SELECT 'why$1', $2 and failed.
|
|
408
|
+
#
|
|
409
|
+
# Called ONLY when there are parameters to bind. SQL with no parameters is
|
|
410
|
+
# sent exactly as written (the cross-framework contract), so the jsonb
|
|
411
|
+
# operators ?, ?| and ?& work in a parameterless query.
|
|
412
|
+
# tina4: with parameters, use jsonb_exists(), jsonb_exists_any() or
|
|
413
|
+
# jsonb_exists_all() instead of ?, ?| or ?& - here every bare `?` is a
|
|
414
|
+
# placeholder.
|
|
387
415
|
def convert_placeholders(sql)
|
|
388
|
-
|
|
389
|
-
sql.gsub("?") { counter += 1; "$#{counter}" }
|
|
416
|
+
Tina4::SQLTranslator.replace_placeholders(sql) { |index| "$#{index + 1}" }
|
|
390
417
|
end
|
|
391
418
|
|
|
392
419
|
# Hydrate a PG::Result into an array of symbol-keyed row hashes.
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Drivers
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require_relative "schema_split"
|
|
10
|
+
require_relative "../sqlite3_gem"
|
|
4
11
|
|
|
5
12
|
module Tina4
|
|
6
13
|
module Drivers
|
|
@@ -27,7 +34,7 @@ module Tina4
|
|
|
27
34
|
# hang. Every other driver's bound lives in its own connect; this is the
|
|
28
35
|
# one that has nothing to bind.
|
|
29
36
|
def connect(connection_string, username: nil, password: nil)
|
|
30
|
-
|
|
37
|
+
Tina4.require_sqlite3!
|
|
31
38
|
db_path = self.class.resolve_path(connection_string)
|
|
32
39
|
|
|
33
40
|
@connection = SQLite3::Database.new(db_path)
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
# Ultipa graph adapter — the portable core built in GQL over tina4-ultipa.
|
|
4
10
|
#
|