woods 1.6.4 → 2.0.0.beta1
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 +1879 -37
- data/CONTRIBUTING.md +195 -137
- data/README.md +162 -520
- data/SECURITY.md +92 -0
- data/assets/woods-wordmark-white-with-bg.png +0 -0
- data/docs/AGENT_GUIDE.md +204 -0
- data/docs/AGENT_SETUP.md +205 -0
- data/docs/BACKEND_MATRIX.md +470 -0
- data/docs/CONFIGURATION_REFERENCE.md +620 -0
- data/docs/CONSOLE_MCP_SETUP.md +829 -0
- data/docs/DOCKER_SETUP.md +454 -0
- data/docs/EMBEDDING_MODELS.md +136 -0
- data/docs/EVALUATION.md +91 -0
- data/docs/EXTRACTOR_REFERENCE.md +765 -0
- data/docs/FAQ.md +544 -0
- data/docs/GETTING_STARTED.md +183 -0
- data/docs/INCREMENTAL_EXTRACTION.md +415 -0
- data/docs/INTERNALS.md +415 -0
- data/docs/MCP_HTTP_TRANSPORT.md +144 -0
- data/docs/MCP_SERVERS.md +231 -0
- data/docs/MCP_TOOL_COOKBOOK.md +987 -0
- data/docs/MCP_WORKTREE_SETUP.md +127 -0
- data/docs/NOTION_INTEGRATION.md +283 -0
- data/docs/OBSIDIAN_INTEGRATION.md +170 -0
- data/docs/PUBLISHED_INDEX.md +197 -0
- data/docs/README.md +94 -0
- data/docs/RETRIEVAL_GUIDE.md +267 -0
- data/docs/TOKEN_BENCHMARK.md +68 -0
- data/docs/TROUBLESHOOTING.md +841 -0
- data/docs/UNBLOCKED_INTEGRATION.md +279 -0
- data/docs/UPGRADING_TO_2.md +321 -0
- data/docs/WATCH_DAEMON.md +667 -0
- data/docs/WHY_WOODS.md +219 -0
- data/exe/woods-console +39 -3
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +78 -24
- data/exe/woods-mcp-start +57 -52
- data/lib/generators/woods/install_generator.rb +6 -5
- data/lib/generators/woods/pgvector_generator.rb +6 -3
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
- data/lib/generators/woods/templates/woods.rb.tt +49 -28
- data/lib/tasks/woods.rake +622 -168
- data/lib/tasks/woods_checks.rake +107 -0
- data/lib/tasks/woods_evaluation.rake +164 -80
- data/lib/woods/ast/call_site_extractor.rb +6 -15
- data/lib/woods/ast/method_extractor.rb +19 -9
- data/lib/woods/ast/parser.rb +54 -8
- data/lib/woods/atomic_file.rb +40 -1
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +18 -13
- data/lib/woods/cache/cache_store.rb +9 -1
- data/lib/woods/cache/solid_cache_store.rb +6 -4
- data/lib/woods/change_set.rb +88 -0
- data/lib/woods/checks/generation_resolution.rb +34 -0
- data/lib/woods/checks/moved_messages.rb +186 -0
- data/lib/woods/chunking/semantic_chunker.rb +160 -18
- data/lib/woods/console/audit_logger.rb +12 -3
- data/lib/woods/console/bridge_protocol.rb +3 -16
- data/lib/woods/console/connection_manager.rb +51 -136
- data/lib/woods/console/credential_index.rb +5 -53
- data/lib/woods/console/credential_scanner.rb +15 -16
- data/lib/woods/console/dispatch_pipeline.rb +46 -34
- data/lib/woods/console/embedded_executor.rb +806 -257
- data/lib/woods/console/eval_guard.rb +27 -20
- data/lib/woods/console/input_contract.rb +78 -0
- data/lib/woods/console/model_validator.rb +24 -6
- data/lib/woods/console/rack_middleware.rb +62 -63
- data/lib/woods/console/redactor.rb +10 -24
- data/lib/woods/console/safe_context.rb +45 -45
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +136 -267
- data/lib/woods/console/sql_noise_stripper.rb +20 -51
- data/lib/woods/console/sql_table_scanner.rb +39 -90
- data/lib/woods/console/sql_validator.rb +455 -85
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +462 -88
- data/lib/woods/console/tools/tier1.rb +0 -3
- data/lib/woods/console/tools/tier4.rb +17 -7
- data/lib/woods/coordination/lock_heartbeat.rb +103 -0
- data/lib/woods/coordination/pipeline_lock.rb +263 -53
- data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
- data/lib/woods/db/migrator.rb +3 -9
- data/lib/woods/db/schema_version.rb +47 -2
- data/lib/woods/dependency_graph.rb +898 -64
- data/lib/woods/embedding/fake.rb +138 -0
- data/lib/woods/embedding/indexer.rb +832 -40
- data/lib/woods/embedding/openai.rb +77 -19
- data/lib/woods/embedding/provider.rb +189 -11
- data/lib/woods/embedding/text_preparer.rb +1 -1
- data/lib/woods/embedding/token_counter.rb +0 -7
- data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
- data/lib/woods/evaluation/ablation_executor.rb +67 -0
- data/lib/woods/evaluation/ablation_provenance.rb +38 -0
- data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
- data/lib/woods/evaluation/ablation_runner.rb +173 -0
- data/lib/woods/evaluation/ablation_summary.rb +65 -0
- data/lib/woods/evaluation/ablation_task.rb +66 -0
- data/lib/woods/evaluation/ablation_task_set.rb +77 -0
- data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
- data/lib/woods/evaluation/ablation_worktree.rb +71 -0
- data/lib/woods/evaluation/baseline.rb +60 -0
- data/lib/woods/evaluation/baseline_runner.rb +11 -3
- data/lib/woods/evaluation/evaluator.rb +41 -8
- data/lib/woods/evaluation/query_set.rb +79 -13
- data/lib/woods/evaluation/report_generator.rb +20 -1
- data/lib/woods/export/unit_facts.rb +0 -11
- data/lib/woods/extracted_unit.rb +22 -63
- data/lib/woods/extractor.rb +2503 -192
- data/lib/woods/extractors/action_cable_extractor.rb +9 -4
- data/lib/woods/extractors/ast_source_extraction.rb +20 -2
- data/lib/woods/extractors/caching_extractor.rb +46 -12
- data/lib/woods/extractors/callback_analyzer.rb +39 -9
- data/lib/woods/extractors/component_discovery.rb +123 -0
- data/lib/woods/extractors/concern_extractor.rb +17 -3
- data/lib/woods/extractors/controller_extractor.rb +389 -29
- data/lib/woods/extractors/decorator_extractor.rb +7 -14
- data/lib/woods/extractors/engine_extractor.rb +53 -8
- data/lib/woods/extractors/event_extractor.rb +55 -4
- data/lib/woods/extractors/factory_extractor.rb +49 -11
- data/lib/woods/extractors/graphql_extractor.rb +162 -66
- data/lib/woods/extractors/i18n_extractor.rb +6 -1
- data/lib/woods/extractors/job_extractor.rb +51 -21
- data/lib/woods/extractors/lib_extractor.rb +23 -17
- data/lib/woods/extractors/line_neutralizer.rb +171 -0
- data/lib/woods/extractors/mailer_extractor.rb +9 -1
- data/lib/woods/extractors/manager_extractor.rb +19 -2
- data/lib/woods/extractors/migration_extractor.rb +22 -11
- data/lib/woods/extractors/model_extractor.rb +292 -57
- data/lib/woods/extractors/package_extractor.rb +154 -0
- data/lib/woods/extractors/phlex_extractor.rb +18 -3
- data/lib/woods/extractors/policy_extractor.rb +6 -5
- data/lib/woods/extractors/poro_extractor.rb +13 -14
- data/lib/woods/extractors/pundit_extractor.rb +3 -3
- data/lib/woods/extractors/rails_source_extractor.rb +24 -7
- data/lib/woods/extractors/rake_task_extractor.rb +158 -30
- data/lib/woods/extractors/reference_patterns.rb +38 -0
- data/lib/woods/extractors/route_extractor.rb +58 -2
- data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
- data/lib/woods/extractors/serializer_extractor.rb +3 -4
- data/lib/woods/extractors/service_extractor.rb +11 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
- data/lib/woods/extractors/shared_utility_methods.rb +36 -6
- data/lib/woods/extractors/source_nesting.rb +560 -0
- data/lib/woods/extractors/state_machine_extractor.rb +30 -18
- data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
- data/lib/woods/extractors/view_component_extractor.rb +28 -3
- data/lib/woods/extractors/view_engines/erb.rb +17 -3
- data/lib/woods/feedback/gap_detector.rb +9 -3
- data/lib/woods/feedback/store.rb +7 -1
- data/lib/woods/filename_utils.rb +29 -1
- data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
- data/lib/woods/flow_assembler.rb +63 -21
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +138 -22
- data/lib/woods/gem_mapper.rb +285 -0
- data/lib/woods/generation.rb +185 -0
- data/lib/woods/git_command.rb +38 -0
- data/lib/woods/git_provenance.rb +16 -2
- data/lib/woods/graph_analyzer.rb +408 -34
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +92 -22
- data/lib/woods/mcp/bootstrap_state.rb +77 -0
- data/lib/woods/mcp/bootstrapper.rb +582 -77
- data/lib/woods/mcp/config_resolver.rb +66 -6
- data/lib/woods/mcp/errors.rb +60 -0
- data/lib/woods/mcp/index_reader.rb +836 -117
- data/lib/woods/mcp/index_reader_pinning.rb +78 -0
- data/lib/woods/mcp/origin_guard.rb +108 -23
- data/lib/woods/mcp/protocol_policy.rb +98 -0
- data/lib/woods/mcp/provider_probe.rb +45 -6
- data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
- data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
- data/lib/woods/mcp/server.rb +907 -154
- data/lib/woods/mcp/tasks/extension.rb +196 -0
- data/lib/woods/mcp/tasks/request_capture.rb +45 -0
- data/lib/woods/mcp/tasks/store.rb +518 -0
- data/lib/woods/mcp/tool_contract.rb +171 -0
- data/lib/woods/mcp/tool_response_renderer.rb +7 -0
- data/lib/woods/mcp/version_aware_tool_dispatch.rb +3 -9
- data/lib/woods/model_name_cache.rb +19 -1
- data/lib/woods/notion/client.rb +132 -36
- data/lib/woods/notion/exporter.rb +456 -61
- data/lib/woods/notion/mappers/column_mapper.rb +34 -5
- data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
- data/lib/woods/notion/mappers/model_mapper.rb +21 -6
- data/lib/woods/notion/mappers/shared.rb +45 -3
- data/lib/woods/notion/sync_manifest.rb +258 -0
- data/lib/woods/obsidian/errors.rb +6 -0
- data/lib/woods/obsidian/name_mapper.rb +40 -24
- data/lib/woods/obsidian/vault_exporter.rb +103 -36
- data/lib/woods/operator/pipeline_guard.rb +118 -21
- data/lib/woods/operator/status_reporter.rb +20 -3
- data/lib/woods/path_dispatcher.rb +276 -0
- data/lib/woods/payload_store.rb +223 -0
- data/lib/woods/published_index/edge_shaper.rb +61 -0
- data/lib/woods/published_index/generation_catalog.rb +72 -0
- data/lib/woods/published_index/typed_unit_reader.rb +48 -0
- data/lib/woods/published_index.rb +287 -0
- data/lib/woods/railtie.rb +70 -38
- data/lib/woods/railtie_support.rb +167 -0
- data/lib/woods/release.rb +12 -0
- data/lib/woods/reload_policy.rb +206 -0
- data/lib/woods/resilience/circuit_breaker.rb +47 -8
- data/lib/woods/resilience/index_validator.rb +296 -10
- data/lib/woods/resilience/retryable_provider.rb +71 -6
- data/lib/woods/resolved_config.rb +55 -11
- data/lib/woods/retrieval/context_assembler.rb +132 -40
- data/lib/woods/retrieval/query_classifier.rb +25 -6
- data/lib/woods/retrieval/ranker.rb +193 -28
- data/lib/woods/retrieval/search_executor.rb +206 -39
- data/lib/woods/retriever.rb +317 -71
- data/lib/woods/retry_after.rb +22 -2
- data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
- data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
- data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +54 -11
- data/lib/woods/session_tracer/session_flow_document.rb +52 -6
- data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
- data/lib/woods/session_tracer/store.rb +14 -1
- data/lib/woods/storage/metadata_store.rb +230 -26
- data/lib/woods/storage/pgvector.rb +180 -22
- data/lib/woods/storage/qdrant.rb +367 -41
- data/lib/woods/storage/snapshotter/metadata.rb +79 -16
- data/lib/woods/storage/snapshotter/vector.rb +128 -17
- data/lib/woods/storage/snapshotter.rb +23 -5
- data/lib/woods/storage/vector_store.rb +49 -8
- data/lib/woods/storage_identity.rb +28 -0
- data/lib/woods/tasks.rb +53 -2
- data/lib/woods/temporal/json_snapshot_store.rb +112 -42
- data/lib/woods/temporal/snapshot_store.rb +139 -42
- data/lib/woods/unblocked/client.rb +119 -17
- data/lib/woods/unblocked/document_builder.rb +34 -2
- data/lib/woods/unblocked/exporter.rb +63 -27
- data/lib/woods/unblocked/rate_limiter.rb +23 -9
- data/lib/woods/unblocked/sync_manifest.rb +16 -8
- data/lib/woods/update_check.rb +24 -1
- data/lib/woods/util/uuid5.rb +124 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/daemon.rb +1345 -0
- data/lib/woods/watch/listen_watcher.rb +81 -0
- data/lib/woods/watch/polling_watcher.rb +137 -0
- data/lib/woods/watch/status.rb +169 -0
- data/lib/woods/watch/tree_scan.rb +163 -0
- data/lib/woods/watch/watcher.rb +100 -0
- data/lib/woods.rb +53 -9
- data/plugin/.claude-plugin/plugin.json +18 -0
- data/plugin/hooks/hooks.json +29 -0
- data/plugin/hooks/woods-post-edit.sh +226 -0
- data/plugin/hooks/woods-session-start.sh +77 -0
- data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
- data/plugin/skills/woods-diagnose/SKILL.md +75 -0
- data/plugin/skills/woods-investigate/SKILL.md +39 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
- data/plugin/skills/woods-setup/SKILL.md +99 -0
- metadata +102 -30
- data/lib/woods/console/adapter_family.rb +0 -39
- data/lib/woods/console/adapters/cache_adapter.rb +0 -58
- data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
- data/lib/woods/console/adapters/job_adapter.rb +0 -74
- data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
- data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
- data/lib/woods/console/bridge.rb +0 -210
- data/lib/woods/console/credential_scanner_registry.rb +0 -36
- data/lib/woods/console/encrypted_credential_snapshot.rb +0 -16
- data/lib/woods/console/sql_output_policy.rb +0 -535
- data/lib/woods/console/sqlite_read_guard.rb +0 -46
- data/lib/woods/formatting/claude_adapter.rb +0 -98
- data/lib/woods/formatting/generic_adapter.rb +0 -56
- data/lib/woods/formatting/gpt_adapter.rb +0 -64
- data/lib/woods/mcp/http_transport_options.rb +0 -15
- data/lib/woods/mcp/origin_policy.rb +0 -113
- data/lib/woods/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- data/lib/woods/observability/instrumentation.rb +0 -34
|
@@ -30,8 +30,8 @@ module Woods
|
|
|
30
30
|
# newline-separated statement structure is preserved for callers that
|
|
31
31
|
# check for multiple statements.
|
|
32
32
|
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
33
|
+
# Block comments are non-nested — real SQL engines do not support nested
|
|
34
|
+
# block comments, and neither does this stripper.
|
|
35
35
|
#
|
|
36
36
|
# @param sql [String] the SQL string to process
|
|
37
37
|
# @return [String] a new string with all SQL comments removed
|
|
@@ -40,7 +40,7 @@ module Woods
|
|
|
40
40
|
|
|
41
41
|
def self.strip_comments(sql)
|
|
42
42
|
out = sql.gsub(LINE_COMMENT, '')
|
|
43
|
-
out.gsub(BLOCK_COMMENT, '
|
|
43
|
+
out.gsub(BLOCK_COMMENT, '')
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
# Strips single-quoted string literals and (for the `:postgres` dialect)
|
|
@@ -53,21 +53,20 @@ module Woods
|
|
|
53
53
|
# single-quote scanner.
|
|
54
54
|
#
|
|
55
55
|
# @param sql [String] the SQL string to process
|
|
56
|
-
# @param dialect [Symbol] `:postgres` (default)
|
|
56
|
+
# @param dialect [Symbol] `:postgres` (default) or `:mysql`.
|
|
57
57
|
# - `:postgres` — single-quoted strings support `''` as an apostrophe
|
|
58
58
|
# escape. Backslash is treated literally and does not escape quotes.
|
|
59
59
|
# Dollar-quoted strings (`$$...$$`, `$tag$...$tag$`) are also stripped.
|
|
60
60
|
# - `:mysql` — single-quoted strings support both `\'` (backslash-escape)
|
|
61
61
|
# and `''` (doubled-quote) as apostrophe escapes. Dollar-quoted strings
|
|
62
62
|
# are not recognized by the combined MySQL security scanner.
|
|
63
|
-
# - `:sqlite` — doubled apostrophes escape strings; backslashes and dollar signs are literal.
|
|
64
63
|
# @return [String] a new string with all string literals replaced by `''`
|
|
65
64
|
# @raise [ArgumentError] if an unsupported dialect is provided
|
|
66
65
|
DOLLAR_QUOTED = /\$(\w*)\$.*?\$\1\$/m
|
|
67
66
|
SINGLE_QUOTED_POSTGRES = /'(?:''|[^'])*'/m
|
|
68
67
|
SINGLE_QUOTED_MYSQL = /'(?:\\.|''|[^'])*'/m
|
|
69
68
|
|
|
70
|
-
SUPPORTED_DIALECTS = %i[postgres mysql
|
|
69
|
+
SUPPORTED_DIALECTS = %i[postgres mysql].freeze
|
|
71
70
|
private_constant :SUPPORTED_DIALECTS
|
|
72
71
|
|
|
73
72
|
def self.strip_literals(sql, dialect: :postgres)
|
|
@@ -77,7 +76,7 @@ module Woods
|
|
|
77
76
|
|
|
78
77
|
# Strip dollar-quoted strings first so stray apostrophes inside them
|
|
79
78
|
# do not interfere with the single-quote scanner.
|
|
80
|
-
out =
|
|
79
|
+
out = sql.gsub(DOLLAR_QUOTED, "''")
|
|
81
80
|
|
|
82
81
|
pattern = dialect == :mysql ? SINGLE_QUOTED_MYSQL : SINGLE_QUOTED_POSTGRES
|
|
83
82
|
out.gsub(pattern, "''")
|
|
@@ -101,10 +100,9 @@ module Woods
|
|
|
101
100
|
# `:mysql` dialect — PostgreSQL does not treat `#` as a comment, and
|
|
102
101
|
# collapsing it there would hide SQL that a real PostgreSQL server
|
|
103
102
|
# still executes. A MySQL `/*! ... */` executable comment is left
|
|
104
|
-
#
|
|
105
|
-
# its body
|
|
106
|
-
# scan
|
|
107
|
-
# visible under `:postgres` too is over-detection at
|
|
103
|
+
# untouched (not treated as a comment at all, under either dialect):
|
|
104
|
+
# MySQL runs its body, so it must stay visible to every downstream
|
|
105
|
+
# scan; leaving it visible under `:postgres` too is over-detection at
|
|
108
106
|
# worst, never under-detection, on a server where it really is inert.
|
|
109
107
|
# An ordinary `/* ... */` block comment is replaced by a single
|
|
110
108
|
# newline rather than vanishing outright, mirroring how a `--`/`#`
|
|
@@ -114,7 +112,7 @@ module Woods
|
|
|
114
112
|
# still reads as following a boundary once comments are gone.
|
|
115
113
|
#
|
|
116
114
|
# @param sql [String] the SQL string to process
|
|
117
|
-
# @param dialect [Symbol] `:postgres` (default)
|
|
115
|
+
# @param dialect [Symbol] `:postgres` (default) or `:mysql` — controls
|
|
118
116
|
# single-quote escape rules (see {.strip_literals}) and whether `#`
|
|
119
117
|
# opens a line comment. MySQL quote flags reflect session sql_mode.
|
|
120
118
|
# @return [String] a new string with comments removed and every string
|
|
@@ -136,8 +134,7 @@ module Woods
|
|
|
136
134
|
if ch == "'"
|
|
137
135
|
close = single_quote_end(
|
|
138
136
|
sql, i,
|
|
139
|
-
backslash_escapes: (mysql && !no_backslash_escapes) ||
|
|
140
|
-
(dialect == :postgres && postgres_escape_string?(sql, i))
|
|
137
|
+
backslash_escapes: (mysql && !no_backslash_escapes) || (!mysql && postgres_escape_string?(sql, i))
|
|
141
138
|
)
|
|
142
139
|
if close
|
|
143
140
|
out << "''"
|
|
@@ -161,7 +158,7 @@ module Woods
|
|
|
161
158
|
out << ch
|
|
162
159
|
i += 1
|
|
163
160
|
end
|
|
164
|
-
elsif
|
|
161
|
+
elsif mysql && ch == '`'
|
|
165
162
|
close = quoted_span_end(sql, i, quote: '`', backslash_escapes: false)
|
|
166
163
|
if close
|
|
167
164
|
# Backticks delimit identifiers. Preserve the token for table
|
|
@@ -173,7 +170,7 @@ module Woods
|
|
|
173
170
|
out << ch
|
|
174
171
|
i += 1
|
|
175
172
|
end
|
|
176
|
-
elsif
|
|
173
|
+
elsif !mysql && ch == '$' && !preceded_by_word_char?(sql, i) && (tag = dollar_tag_at(sql, i))
|
|
177
174
|
close = sql.index(tag, i + tag.length)
|
|
178
175
|
if close
|
|
179
176
|
out << "''"
|
|
@@ -185,22 +182,8 @@ module Woods
|
|
|
185
182
|
elsif dash_comment?(sql, i, mysql: mysql) || (mysql && ch == '#')
|
|
186
183
|
nl = sql.index("\n", i)
|
|
187
184
|
i = nl || len
|
|
188
|
-
elsif sql[i
|
|
189
|
-
close =
|
|
190
|
-
if close
|
|
191
|
-
comment = sql[i...close]
|
|
192
|
-
yield comment if block_given?
|
|
193
|
-
# An executable comment is a lexical boundary even when its
|
|
194
|
-
# version guard makes its body inert. A quote in that body must
|
|
195
|
-
# never change the scanner's state outside the comment.
|
|
196
|
-
out << comment
|
|
197
|
-
i = close
|
|
198
|
-
else
|
|
199
|
-
out << ch
|
|
200
|
-
i += 1
|
|
201
|
-
end
|
|
202
|
-
elsif ch == '/' && sql[i + 1] == '*'
|
|
203
|
-
close = block_comment_end(sql, i, nested: dialect == :postgres)
|
|
185
|
+
elsif ch == '/' && sql[i + 1] == '*' && sql[i + 2] != '!'
|
|
186
|
+
close = sql.index('*/', i + 2)
|
|
204
187
|
if close
|
|
205
188
|
# Preserve a newline in place of the removed comment, mirroring
|
|
206
189
|
# line comments (see class docs): callers that check for
|
|
@@ -208,7 +191,7 @@ module Woods
|
|
|
208
191
|
# need a survivable marker showing a comment sat here, the
|
|
209
192
|
# same way a `--`/`#` comment's own trailing newline does.
|
|
210
193
|
out << "\n"
|
|
211
|
-
i = close
|
|
194
|
+
i = close + 2
|
|
212
195
|
else
|
|
213
196
|
# Unterminated block comment: never under-detect. Leave it in
|
|
214
197
|
# place (over-detection is safe; the old regex also required a
|
|
@@ -225,20 +208,6 @@ module Woods
|
|
|
225
208
|
out
|
|
226
209
|
end
|
|
227
210
|
|
|
228
|
-
def self.block_comment_end(sql, start, nested:)
|
|
229
|
-
depth = 1
|
|
230
|
-
position = start + 2
|
|
231
|
-
while (match = %r{/\*|\*/}.match(sql, position))
|
|
232
|
-
depth += 1 if nested && match[0] == '/*'
|
|
233
|
-
depth -= 1 if match[0] == '*/'
|
|
234
|
-
return match.end(0) if depth.zero?
|
|
235
|
-
|
|
236
|
-
position = match.end(0)
|
|
237
|
-
end
|
|
238
|
-
nil
|
|
239
|
-
end
|
|
240
|
-
private_class_method :block_comment_end
|
|
241
|
-
|
|
242
211
|
# Session modes change MySQL quoting without changing the adapter name.
|
|
243
212
|
# Security consumers must reject SQL unsafe under any supported combination.
|
|
244
213
|
MYSQL_QUOTE_MODES = [false, true].product([false, true]).map do |ansi, no_backslash|
|
|
@@ -260,7 +229,7 @@ module Woods
|
|
|
260
229
|
|
|
261
230
|
# Regexp matching a PostgreSQL dollar-quote opening tag (`$$` or
|
|
262
231
|
# `$tag$`) at the start of the given slice.
|
|
263
|
-
DOLLAR_TAG = /\A
|
|
232
|
+
DOLLAR_TAG = /\A\$\w*\$/
|
|
264
233
|
private_constant :DOLLAR_TAG
|
|
265
234
|
|
|
266
235
|
# Return the dollar-quote tag opening at +index+, or nil.
|
|
@@ -273,7 +242,7 @@ module Woods
|
|
|
273
242
|
private_class_method :dollar_tag_at
|
|
274
243
|
|
|
275
244
|
# Whether the character immediately before +index+ is a word character
|
|
276
|
-
# (
|
|
245
|
+
# (`\w`). PostgreSQL allows `$` inside identifiers (`x$a$`), so a `$`
|
|
277
246
|
# is only a candidate dollar-quote opener when it does NOT follow an
|
|
278
247
|
# identifier character — otherwise `x$a$ FROM blocked, (SELECT 1 AS
|
|
279
248
|
# z$a$)` gets misread as one dollar-quoted literal spanning the real
|
|
@@ -281,7 +250,7 @@ module Woods
|
|
|
281
250
|
#
|
|
282
251
|
# @api private
|
|
283
252
|
def self.preceded_by_word_char?(sql, index)
|
|
284
|
-
index.positive? && sql[index - 1].match?(/
|
|
253
|
+
index.positive? && sql[index - 1].match?(/\w/)
|
|
285
254
|
end
|
|
286
255
|
private_class_method :preceded_by_word_char?
|
|
287
256
|
|
|
@@ -332,7 +301,7 @@ module Woods
|
|
|
332
301
|
def self.postgres_escape_string?(sql, quote_index)
|
|
333
302
|
return false unless quote_index.positive? && sql[quote_index - 1].match?(/[eE]/)
|
|
334
303
|
|
|
335
|
-
quote_index < 2 || !sql[quote_index - 2].match?(/[A-Za-z0-9_
|
|
304
|
+
quote_index < 2 || !sql[quote_index - 2].match?(/[A-Za-z0-9_$]/)
|
|
336
305
|
end
|
|
337
306
|
private_class_method :postgres_escape_string?
|
|
338
307
|
end # rubocop:enable Metrics/ModuleLength
|
|
@@ -38,20 +38,20 @@ module Woods
|
|
|
38
38
|
# identifier so it does not hide the table name. ANSI-89 comma joins
|
|
39
39
|
# are handled separately — see FROM_CLAUSE.
|
|
40
40
|
JOIN_REFERENCE = /
|
|
41
|
-
\b(?:STRAIGHT_)?JOIN
|
|
42
|
-
(?:ONLY
|
|
41
|
+
\b(?:STRAIGHT_)?JOIN\s+
|
|
42
|
+
(?:ONLY\s+)?
|
|
43
43
|
(?:
|
|
44
44
|
(?:
|
|
45
|
-
`(?<jschema_bt>
|
|
46
|
-
"(?<jschema_dq>
|
|
47
|
-
(?<jschema_bare
|
|
45
|
+
`(?<jschema_bt>[^`]+)` |
|
|
46
|
+
"(?<jschema_dq>[^"]+)" |
|
|
47
|
+
(?<jschema_bare>\w+)
|
|
48
48
|
)
|
|
49
49
|
\s* \. \s*
|
|
50
50
|
)?
|
|
51
51
|
(?:
|
|
52
|
-
`(?<backtick>
|
|
53
|
-
"(?<double>
|
|
54
|
-
(?<bare
|
|
52
|
+
`(?<backtick>[^`]+)` |
|
|
53
|
+
"(?<double>[^"]+)" |
|
|
54
|
+
(?<bare>\w+(?:\.\w+)?)
|
|
55
55
|
)
|
|
56
56
|
/xi
|
|
57
57
|
|
|
@@ -66,7 +66,7 @@ module Woods
|
|
|
66
66
|
# every `FROM` as its own independent scan match is what keeps CTEs,
|
|
67
67
|
# UNIONs, and nested subqueries in coverage.
|
|
68
68
|
FROM_CLAUSE = /
|
|
69
|
-
\bFROM
|
|
69
|
+
\bFROM\s+
|
|
70
70
|
(?<clause>.+?)
|
|
71
71
|
(?=
|
|
72
72
|
\b(?:WHERE|GROUP|HAVING|ORDER|LIMIT|OFFSET|UNION|INTERSECT|EXCEPT|
|
|
@@ -85,16 +85,16 @@ module Woods
|
|
|
85
85
|
\A
|
|
86
86
|
(?:
|
|
87
87
|
(?:
|
|
88
|
-
`(?<schema_bt>
|
|
89
|
-
"(?<schema_dq>
|
|
90
|
-
(?<schema_bare
|
|
88
|
+
`(?<schema_bt>[^`]+)` |
|
|
89
|
+
"(?<schema_dq>[^"]+)" |
|
|
90
|
+
(?<schema_bare>\w+)
|
|
91
91
|
)
|
|
92
92
|
\s* \. \s*
|
|
93
93
|
)?
|
|
94
94
|
(?:
|
|
95
|
-
`(?<backtick>
|
|
96
|
-
"(?<double>
|
|
97
|
-
(?<bare
|
|
95
|
+
`(?<backtick>[^`]+)` |
|
|
96
|
+
"(?<double>[^"]+)" |
|
|
97
|
+
(?<bare>\w+(?:\.\w+)?)
|
|
98
98
|
)
|
|
99
99
|
/xi
|
|
100
100
|
|
|
@@ -102,7 +102,7 @@ module Woods
|
|
|
102
102
|
# identifier. Strip it so the lead-identifier regex sees the table
|
|
103
103
|
# directly. Anchored with `\A` because callers strip leading whitespace
|
|
104
104
|
# first via #strip.
|
|
105
|
-
ONLY_PREFIX = /\AONLY
|
|
105
|
+
ONLY_PREFIX = /\AONLY\s+/i
|
|
106
106
|
|
|
107
107
|
# Matches a MySQL executable comment (`/*!...*/` or the version-guarded
|
|
108
108
|
# `/*!NNNNN...*/`), capturing the body. Mirrors
|
|
@@ -110,7 +110,7 @@ module Woods
|
|
|
110
110
|
# deliberately leaves these markers in place because their meaning is
|
|
111
111
|
# version-dependent; .executable_comment_views scans both of their
|
|
112
112
|
# possible semantics.
|
|
113
|
-
EXECUTABLE_COMMENT_PATTERN = %r{
|
|
113
|
+
EXECUTABLE_COMMENT_PATTERN = %r{/\*!(?:\d{5})?(.*?)\*/}m
|
|
114
114
|
|
|
115
115
|
# Matches the standalone SQL `TABLE name` statement (PostgreSQL, and
|
|
116
116
|
# MySQL 8.0.19+) — shorthand for `SELECT * FROM name`. It appears as a
|
|
@@ -119,20 +119,20 @@ module Woods
|
|
|
119
119
|
# scanned independently of FROM_CLAUSE/JOIN_REFERENCE rather than as
|
|
120
120
|
# part of either. The identifier grammar mirrors LEAD_IDENT.
|
|
121
121
|
TABLE_STATEMENT = /
|
|
122
|
-
\bTABLE
|
|
123
|
-
(?:ONLY
|
|
122
|
+
\bTABLE\s+
|
|
123
|
+
(?:ONLY\s+)?
|
|
124
124
|
(?:
|
|
125
125
|
(?:
|
|
126
|
-
`(?<schema_bt>
|
|
127
|
-
"(?<schema_dq>
|
|
128
|
-
(?<schema_bare
|
|
126
|
+
`(?<schema_bt>[^`]+)` |
|
|
127
|
+
"(?<schema_dq>[^"]+)" |
|
|
128
|
+
(?<schema_bare>\w+)
|
|
129
129
|
)
|
|
130
130
|
\s* \. \s*
|
|
131
131
|
)?
|
|
132
132
|
(?:
|
|
133
|
-
`(?<backtick>
|
|
134
|
-
"(?<double>
|
|
135
|
-
(?<bare
|
|
133
|
+
`(?<backtick>[^`]+)` |
|
|
134
|
+
"(?<double>[^"]+)" |
|
|
135
|
+
(?<bare>\w+(?:\.\w+)?)
|
|
136
136
|
)
|
|
137
137
|
/xi
|
|
138
138
|
|
|
@@ -167,51 +167,6 @@ module Woods
|
|
|
167
167
|
results.uniq
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
-
# Table-factor prefixes, retaining commas after balanced subqueries and
|
|
171
|
-
# JOIN predicates. Each nested FROM/JOIN is also scanned independently.
|
|
172
|
-
# SQL punctuation separates tokens without mandatory whitespace. Keep
|
|
173
|
-
# compact quoted targets and parenthesized relations in the same policy.
|
|
174
|
-
# @param sql [String] noise-stripped SQL
|
|
175
|
-
# @return [Array<String>]
|
|
176
|
-
def self.relation_factors(sql)
|
|
177
|
-
sql.to_enum(:scan, /\b(?:FROM|(?:STRAIGHT_)?JOIN)(?=[\s("`])\s*/i).flat_map do
|
|
178
|
-
suffix = sql[Regexp.last_match.end(0)..]
|
|
179
|
-
split_top_level_commas(relation_clause(suffix))
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Quoted tokens shield punctuation and clause words; a closing parenthesis
|
|
184
|
-
# at depth zero ends this query's clause, not a nested table expression.
|
|
185
|
-
def self.relation_clause(suffix)
|
|
186
|
-
depth = 0
|
|
187
|
-
tokens = /
|
|
188
|
-
"(?:[^"]|"")*"|`(?:[^`]|``)*`|''|[()]|
|
|
189
|
-
\b(?:WHERE|GROUP|HAVING|ORDER|LIMIT|OFFSET|UNION|INTERSECT|EXCEPT|WINDOW)\b
|
|
190
|
-
/ix
|
|
191
|
-
suffix.to_enum(:scan, tokens).each do
|
|
192
|
-
token = Regexp.last_match[0]
|
|
193
|
-
start = Regexp.last_match.begin(0)
|
|
194
|
-
finish = Regexp.last_match.end(0)
|
|
195
|
-
boundary = token == ')' || relation_keyword_boundary?(suffix[0...start], suffix[finish..], token)
|
|
196
|
-
return suffix[0...start] if depth.zero? && boundary
|
|
197
|
-
|
|
198
|
-
depth += 1 if token == '('
|
|
199
|
-
depth -= 1 if token == ')'
|
|
200
|
-
end
|
|
201
|
-
suffix
|
|
202
|
-
end
|
|
203
|
-
private_class_method :relation_clause
|
|
204
|
-
|
|
205
|
-
def self.relation_keyword_boundary?(prefix, rest, token)
|
|
206
|
-
return false unless token.match?(/\A[A-Za-z]/)
|
|
207
|
-
return false if prefix.strip.empty? || prefix.match?(/(?:,|\bAS)\s*\z/i) || rest.lstrip.start_with?(',')
|
|
208
|
-
return rest.match?(/\A\s+BY\b/i) if %w[GROUP ORDER].include?(token.upcase)
|
|
209
|
-
return rest.match?(/\A\s+[A-Za-z0-9_$\u0080-\u{10ffff}]+\s+AS\b/i) if token.casecmp?('WINDOW')
|
|
210
|
-
|
|
211
|
-
true
|
|
212
|
-
end
|
|
213
|
-
private_class_method :relation_keyword_boundary?
|
|
214
|
-
|
|
215
170
|
# @api private
|
|
216
171
|
# Every view of the stripped SQL the FROM/JOIN scans must consider for
|
|
217
172
|
# MySQL executable comments (`/*! ... */`). {SqlNoiseStripper} leaves
|
|
@@ -248,9 +203,12 @@ module Woods
|
|
|
248
203
|
|
|
249
204
|
# @api private
|
|
250
205
|
def self.collect_from_identifiers(sql, results)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
206
|
+
sql.scan(FROM_CLAUSE) do
|
|
207
|
+
clause = Regexp.last_match[:clause]
|
|
208
|
+
split_top_level_commas(clause).each do |chunk|
|
|
209
|
+
ident = lead_identifier(chunk)
|
|
210
|
+
results << ident if ident
|
|
211
|
+
end
|
|
254
212
|
end
|
|
255
213
|
end
|
|
256
214
|
private_class_method :collect_from_identifiers
|
|
@@ -271,7 +229,7 @@ module Woods
|
|
|
271
229
|
depth = 0
|
|
272
230
|
buf = +''
|
|
273
231
|
parts = []
|
|
274
|
-
clause.
|
|
232
|
+
clause.each_char do |ch|
|
|
275
233
|
case ch
|
|
276
234
|
when '('
|
|
277
235
|
depth += 1
|
|
@@ -301,9 +259,8 @@ module Woods
|
|
|
301
259
|
# PostgreSQL `ONLY` inheritance keyword is stripped first so it does
|
|
302
260
|
# not hide the table.
|
|
303
261
|
def self.lead_identifier(chunk)
|
|
304
|
-
stripped = chunk.to_s.strip.sub(
|
|
305
|
-
|
|
306
|
-
return nil if stripped.empty? || stripped.match?(/\A(?:SELECT|WITH|TABLE)\b/i)
|
|
262
|
+
stripped = chunk.to_s.strip.sub(ONLY_PREFIX, '')
|
|
263
|
+
return nil if stripped.empty?
|
|
307
264
|
|
|
308
265
|
match = LEAD_IDENT.match(stripped)
|
|
309
266
|
return nil unless match
|
|
@@ -316,22 +273,14 @@ module Woods
|
|
|
316
273
|
# Combine a schema prefix with the table identifier captured by
|
|
317
274
|
# JOIN_REFERENCE / LEAD_IDENT into a single `schema.table` string.
|
|
318
275
|
def self.qualified_identifier(match)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
276
|
+
table = match[:backtick] || match[:double] || match[:bare]
|
|
277
|
+
schema = match.named_captures.values_at(
|
|
278
|
+
'schema_bt', 'schema_dq', 'schema_bare',
|
|
279
|
+
'jschema_bt', 'jschema_dq', 'jschema_bare'
|
|
280
|
+
).compact.first
|
|
324
281
|
schema ? "#{schema}.#{table}" : table
|
|
325
282
|
end
|
|
326
283
|
private_class_method :qualified_identifier
|
|
327
|
-
|
|
328
|
-
def self.decoded_identifier(backtick, double, bare)
|
|
329
|
-
return backtick.gsub('``', '`') if backtick
|
|
330
|
-
return double.gsub('""', '"') if double
|
|
331
|
-
|
|
332
|
-
bare
|
|
333
|
-
end
|
|
334
|
-
private_class_method :decoded_identifier
|
|
335
284
|
end
|
|
336
285
|
end
|
|
337
286
|
end
|